Cart, Checkout and Order

The legacy single “Orders” bounded context is decomposed into three independent services with different storage models, data lifetimes, and consistency requirements.

ServiceResponsibilityStorage
cart-serviceEphemeral cart sessions and soft inventory holdsRedis
checkout-serviceStateless orchestration of the cart → order transitionNone (stateless)
order-servicePermanent order recordsPostgreSQL

Cart Service#

cart-service manages ephemeral shopping sessions. It has no PostgreSQL database — all state lives in Redis and expires automatically.

Redis Data Structure#

Each cart is stored as a Redis hash under the key cart:{cartId}. The structure holds:

FieldNotes
storeId
customerIdNULL for guest carts
sessionToken
expiresAtExtended on every mutation
items[]Array of item objects (see below)

Each item in items[] carries:

FieldNotes
itemId
variantId
quantity
snapshot{name, sku, imageUrl, basePrice} — captured at add-to-cart for display fallback only
softReservationIdReturned by inventory-service at add-to-cart

The basePrice in the snapshot is never used for pricing at display time. Current prices are always fetched live from catalog-service on every cart display call.

Add to Cart Flow#

  1. Call catalog-service via gRPC to validate the variant exists and fetch the display snapshot (name, SKU, image URL, base price).
  2. Call inventory-service via gRPC — PlaceSoftReservation(variantId, storeId, quantity, ttlSeconds). If inventory-service returns INSUFFICIENT_STOCK, reject the request with an AVAILABILITY_ERROR. The soft reservation TTL is configurable per store; the default is 30 minutes.
  3. Write the item to Redis with the snapshot and the returned softReservationId. Extend the cart TTL.

Display Cart Flow#

  1. Load all items from Redis.
  2. Call catalog-service via gRPC in a single batch request — GetVariantPrices(variantIds[], priceListId, currency, storeId) — to retrieve current prices for all items. Prices are always live at this step.
  3. Call marketing-service via gRPC — EvaluatePromotions(cartItems, storeId, customerId) — to apply auto-apply promotions and compute line totals and the cart subtotal.
  4. Return the enriched cart with live prices and applied discounts.

Cart TTLs#

Cart TypeTTL
Guest~7 days
Authenticated~30 days

Both TTLs are extended on every cart mutation. Soft reservation TTLs are managed autonomously by inventory-service. When a soft reservation expires, inventory-service releases the stock and publishes inventory.reservation_expired. The next display call to cart-service will reflect the item as unavailable.

Cart Merge on Login#

When a guest user authenticates mid-session, the guest cart replaces the authenticated cart. The authenticated cart is discarded. The guest cart is re-associated to the authenticated customerId. Soft reservations follow the guest cart and are not re-placed.

This policy reflects current intent: the guest cart represents what the user was actively browsing; the authenticated cart may be stale from a previous session.

Guest Checkout#

Guest checkout is store-configurable (enabled or disabled per store setting).

When enabled: the customer provides an email address at the start of checkout. No account is required. A guest UUID is used as the customerId throughout the session. The resulting order record stores both the guest UUID and the email address. After order placement, the storefront may optionally prompt account creation and link the order to the new account.

When disabled: unauthenticated users can build carts but cannot initiate checkout.

Checkout Service#

checkout-service is stateless. It owns no persistent data and holds no database connection. Its sole responsibility is orchestrating the cart → placed order transition as a compensating transaction saga — there is no distributed 2PC.

Inputs#

cartId, customerId (or guestId + guestEmail), shippingAddress, billingAddress, couponCodes[].

Orchestration Sequence#

  1. Validate cart — Call cart-service via gRPC to retrieve the cart with fresh prices. Verify that items are present.
  2. Lock final prices and apply promotions — Call catalog-service via gRPC for an authoritative price lock (GetVariantPricesForCheckout). Call marketing-service via gRPC to apply coupon codes and promotions. This produces the locked line items with all discounts applied.
  3. Upgrade inventory reservations — Call inventory-service via gRPC — UpgradeToHardReservation for all soft holds. If any reservation cannot be upgraded (stock claimed by another checkout since the cart was built), return CART_CONFLICT to the caller. No compensation is needed at this step because no side effects have been committed yet.
  4. Create pending order — Call order-service via gRPC to create an order with status PENDING. The payload includes the full line item snapshots, locked prices, shipping and billing addresses, and applied discounts. order-service returns an orderId.
  5. Process payment — Call payment-service via gRPC — Charge(orderId, customerId, storeId, amount, ...). On payment failure: call inventory-service ReleaseReservations(reservationIds[]) and order-service cancelOrder(orderId), then return PAYMENT_FAILED.
  6. Confirm order — Call order-service via gRPC to set the order status to CONFIRMED and attach the paymentTransactionId.
  7. Clear cart — Call cart-service via gRPC to delete the cart session.
  8. Publish event — Publish order.placed to Kafka.

Compensation Table#

Compensations are attempted in reverse order when a later step fails. If a compensation call itself fails, the checkout is marked COMPENSATION_FAILED and an alert fires through the observability pipeline for manual resolution.

Side EffectCompensation
Hard reservations placed (step 3)Inventory.ReleaseReservations(reservationIds[])
Pending order created (step 4)Order.cancelOrder(orderId)
Payment charged (step 5)Payment.Reverse(transactionId)

Order Service#

order-service is a Quarkus service that owns permanent order records. Storage is PostgreSQL with store_id as a discriminator column for multi-tenant isolation.

Data Model#

orders#

ColumnTypeNotes
order_idUUID PK
store_idUUID
customer_idUUIDGuest UUID for guest checkouts
guest_emailVARCHARPopulated for guest checkouts only
statusENUMSee status transitions below
sourceENUMSTOREFRONT / INTERNAL / EXTERNAL_CHANNEL
shipping_addrJSONBSnapshot at placement
billing_addrJSONBSnapshot at placement
subtotalNUMERIC
shipping_costNUMERIC
tax_amountNUMERIC
totalNUMERIC
payment_refVARCHARpaymentTransactionId from payment-service
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

order_line_items#

ColumnTypeNotes
line_item_idUUID PK
order_idUUID FK
variant_idUUIDReference only — no FK to catalog; catalog records can change
quantityINT
unit_priceNUMERICLocked at checkout; immutable after creation
total_priceNUMERIC
snapshotJSONB{name, sku, imageUrl} captured at checkout

order_discounts#

Columns: type (COUPON or PROMOTION), code, amount.

Immutability#

Order records are immutable after creation. unit_price and all snapshot fields are frozen at the moment checkout-service writes the order. order-service never calls catalog-service after an order is created — all item data is self-contained within the order record.

Status Transitions#

PENDING → CONFIRMED → PROCESSING → SHIPPED → DELIVERED

Cancellation is permitted from PENDING, CONFIRMED, and PROCESSING. Orders that have reached SHIPPED cannot be cancelled. REFUNDED is a terminal status reachable only from DELIVERED.

Kafka Events Published#

TopicConsumers
order.placedinventory-service (consume hard reservations), communication-service (confirmation email/SMS), marketing-service (loyalty points, attribution), BI
order.confirmedFulfillment workflows
order.shippedcommunication-service (shipping notification), customer profile service
order.deliveredcommunication-service (delivery notification), marketing-service (review request trigger)
order.cancelledinventory-service (release reservations if not yet consumed), payment-service (refund if charged), communication-service
order.refundedpayment-service, BI

GraphQL Subgraph#

order-service participates in Apollo Router federation as a subgraph.

Order@key(fields: "id"). Fields: orderStatus, lineItems, total, createdAt.

QuerySignature
orderorder(id: ID!): Order
customerOrderscustomerOrders(customerId: ID!, page: Int, pageSize: Int): OrderConnection

The order query serves the storefront order confirmation page. customerOrders serves the customer order history section of the storefront.

Vault Configuration#

Key pathPurpose
ss3/kv/order-service/db.urlPostgreSQL JDBC URL
ss3/kv/order-service/db.usernameDatabase username
ss3/kv/order-service/db.passwordDatabase password
ss3/kv/shared/Shared Kafka bootstrap and OTel config

Item Data Across Services#

ServicePrice SourcePrice FreshnessStorage
CartCatalog gRPC (GetVariantPrices) at display timeAlways live — fetched on every cart viewRedis per session
CheckoutCatalog gRPC (GetVariantPricesForCheckout) at checkout startAlways live — this is the authoritative price lockTransient in memory
OrderPassed by checkout-service at order creationFrozen at placement — never updatedPostgreSQL, immutable