Best practices

Learn best practices to build a secure, performant, and extensible custom checkout flow.

Copy for LLM
View as Markdown

After completing this page, you should be able to:

  • Apply security best practices to protect sensitive customer data and ensure PCI DSS compliance.

  • Implement idempotency strategies to prevent duplicate orders and payment charges.

  • Optimize checkout performance and design for extensibility to support future integrations.

The checkout process is a critical system for any commerce platform. It:

  • Impacts revenue and conversion rates directly.
  • Handles sensitive personal and payment data.
  • Integrates with multiple external systems, such as payment service providers (PSPs) and enterprise resource planning (ERP) systems.
  • Performs reliably under heavy load.

To build a scalable and maintainable checkout system, it is essential to implement robust architectural patterns early in development.

Security considerations

  • PCI DSS compliance: Never store or transmit raw credit card data. Always use a PSP's SDK to tokenize payment information.
  • OAuth 2.0 for commercetools API: For secure server-to-server API calls, use the client credentials flow. For frontend API calls, use password or anonymous session tokens.
  • PII protection: Encrypt sensitive user data at rest. Limit its exposure in logs and monitoring systems.
  • Input validation: Validate all incoming data payloads to your backend for frontend (BFF) APIs. This prevents injection attacks and malformed data.
  • Session security: Regenerate session tokens upon login to prevent session fixation attacks. Implement cross-site request forgery (CSRF) protection on all state-changing endpoints (POST, PUT, DELETE).

Performance and reliability

  • Minimize API round trips: Batch multiple Cart actions into a single API call, where possible.
  • Configure timeouts and retries: Implement appropriate timeout values and retry strategies for API calls to handle transient failures gracefully. For more information about timeout configuration and retry policies, see Error handling through timeout and retries.
  • Cache static data: Cache data that changes infrequently, such as payment method icons and shipping method details.
  • Use asynchronous processing: Move non-essential post-order tasks, like inventory synchronization or sending confirmation emails, to background jobs to avoid blocking the final checkout step.
  • Implement rate limiting: Implement rate limiting to protect your BFF endpoints from abuse and denial-of-service attacks.

Fraud and abuse prevention

  • Implement bot mitigation strategies: Consider adding CAPTCHA to the checkout if you detect automated abuse.
  • Monitor for suspicious patterns: Track and flag unusual ordering behaviors, such as multiple failed payment attempts or rapid address changes.

Guest versus registered checkout

  • Guest checkout: Offers minimal friction to improve conversion rates. The Cart is tracked using an anonymousId, and you can optionally allow users to create an account after placing an Order. Guest checkouts cannot use the Me endpoints.

    Cart Discounts that target specific Customer Groups do not work for anonymous Carts. As a workaround, you can:

    • Create a placeholder Customer entity that contains a link to the external customer, identifiable through a unique identifier. The placeholder Customer can have Customer Groups, which can be used during the checkout process.
    • Set the Customer Group as a customField on the Cart entity and match the Cart Discount predicate against the Custom Field.
  • Registered checkout: Provides a personalized experience with saved addresses, payment preferences, and order history. The Cart is tracked using a customerId, and it requires a customer to log in.
  • Merge logic: Define clear rules for handling cases where a guest with an active Cart logs in. The best practice is to merge the guest Cart with the customer's existing Cart to ensure that selected items and promotions are preserved.

Customizations and extensibility

To add business-specific data and logic to the checkout flow, use the platform's extensibility features.

  • Custom Fields: Add data to a Cart, Order, or Payment object for needs like gift messages, special delivery instructions, or internal tracking IDs for analytics or order management.
    // For Store-scoped Carts (Carts associated with a Store - recommended)
    async function setCustomCartField(
      storeKey: string,
      cartId: string,
      version: number,
      name: string,
      value: any
    ) {
      return apiRoot
        .inStoreKeyWithStoreKeyValue({ storeKey })
        .carts()
        .withId({ ID: cartId })
        .post({
          body: { version, actions: [{ action: "setCustomField", name, value }] },
        })
        .execute();
    }
    
    // For Global Carts
    async function setGlobalCustomCartField(
      cartId: string,
      version: number,
      name: string,
      value: any
    ) {
      return apiRoot
        .carts()
        .withId({ ID: cartId })
        .post({
          body: { version, actions: [{ action: "setCustomField", name, value }] },
        })
        .execute();
    }
    
  • Custom Types: Define a structured data type for your Custom Fields to ensure data consistency across objects and integrations.
  • Subscriptions: Use for asynchronous, event-driven workflows that do not need to block the user–for example, syncing an Order to an ERP, sending confirmation emails, or triggering fraud checks.

Testing your checkout flow

  • Unit tests: Mock commercetools API responses and external services to validate business logic, such as minimum order values or allowed payment methods.
  • Integration tests: Use a commercetools sandbox project to test the integration between your application and the commercetools platform. Mock PSP APIs or use their provided sandbox credentials.
  • End-to-end (E2E) tests: Use a browser automation tool like Playwright or Cypress to simulate complete user journeys. Test both guest and registered user flows, including payment success and failure scenarios.

Logging and monitoring

  • Implement structured logging and monitoring for all key checkout events.

    Example of Logging a checkout flowts
    function logCheckoutEvent(eventType: string, context: any) {
      console.info(
        JSON.stringify({
          timestamp: new Date().toISOString(),
          event: eventType,
          cartId: context.cartId,
          orderId: context.orderId,
          paymentId: context.paymentId,
          userId: context.userId,
        })
      );
    }
    
  • Keep logs structured (such as, JSON) for effective searching and monitoring in platforms like Datadog or an ELK stack.

  • Avoid logging full personally identifiable information (PII) or payment card details.

High-level checklist

  • Ensure only one active Cart exists per user session during checkout.
  • Handle all sensitive payment data through the PSP; never let it touch your backend.
  • Use the latest Cart version for every update to prevent conflicts.
  • Define and implement clear merge rules for guest-to-customer transitions.
  • Implement robust retry logic for payment and Order creation.
  • Use Subscriptions for asynchronous post-order workflows.

Common pitfalls to avoid

  • Blocking checkout with post-order tasks: Post-order tasks, like sending an order confirmation email, can be done asynchronously to avoid a slow user experience.
  • Ignoring cart merge logic: Can frustrate users who switch devices or log in mid-session, potentially causing them to lose their cart.
  • Not handling asynchronous PSP webhooks: Can result in orders getting stuck in an incorrect state if a payment status updates asynchronously.

Key takeaways

  • Prioritize security: PCI compliance failures and data breaches are costly and damage customer trust.
  • Implement idempotency: It is your safeguard against duplicate charges and orders caused by network instability.
  • Optimize for conversion and loyalty: Support a frictionless guest checkout flow and a feature-rich registered user flow.
  • Design for extensibility: Your architecture should allow for new payment or shipping providers to be added with minimal code changes.
  • Test thoroughly: A multi-layered testing strategy is essential for preventing critical bugs in production.

By following the patterns in this module, your checkout implementation will be both reliable and adaptable, ready to accommodate evolving payment, shipping, and business requirements.

Test your knowledge