The legacy single “Orders” bounded context is decomposed into three independent services with different storage models, data lifetimes, and consistency requirements.
| Service | Responsibility | Storage |
|---|---|---|
cart-service | Ephemeral cart sessions and soft inventory holds | Redis |
checkout-service | Stateless orchestration of the cart → order transition | None (stateless) |
order-service | Permanent order records | PostgreSQL |
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:
| Field | Notes |
|---|---|
storeId | |
customerId | NULL for guest carts |
sessionToken | |
expiresAt | Extended on every mutation |
items[] | Array of item objects (see below) |
Each item in items[] carries:
| Field | Notes |
|---|---|
itemId | |
variantId | |
quantity | |
snapshot | {name, sku, imageUrl, basePrice} — captured at add-to-cart for display fallback only |
softReservationId | Returned 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#
- Call catalog-service via gRPC to validate the variant exists and fetch the display snapshot (name, SKU, image URL, base price).
- Call inventory-service via gRPC —
PlaceSoftReservation(variantId, storeId, quantity, ttlSeconds). If inventory-service returnsINSUFFICIENT_STOCK, reject the request with anAVAILABILITY_ERROR. The soft reservation TTL is configurable per store; the default is 30 minutes. - Write the item to Redis with the snapshot and the returned
softReservationId. Extend the cart TTL.
Display Cart Flow#
- Load all items from Redis.
- 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. - Call marketing-service via gRPC —
EvaluatePromotions(cartItems, storeId, customerId)— to apply auto-apply promotions and compute line totals and the cart subtotal. - Return the enriched cart with live prices and applied discounts.
Cart TTLs#
| Cart Type | TTL |
|---|---|
| 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#
- Validate cart — Call cart-service via gRPC to retrieve the cart with fresh prices. Verify that items are present.
- 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. - Upgrade inventory reservations — Call inventory-service via gRPC —
UpgradeToHardReservationfor all soft holds. If any reservation cannot be upgraded (stock claimed by another checkout since the cart was built), returnCART_CONFLICTto the caller. No compensation is needed at this step because no side effects have been committed yet. - 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 anorderId. - Process payment — Call payment-service via gRPC —
Charge(orderId, customerId, storeId, amount, ...). On payment failure: call inventory-serviceReleaseReservations(reservationIds[])and order-servicecancelOrder(orderId), then returnPAYMENT_FAILED. - Confirm order — Call order-service via gRPC to set the order status to
CONFIRMEDand attach thepaymentTransactionId. - Clear cart — Call cart-service via gRPC to delete the cart session.
- Publish event — Publish
order.placedto 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 Effect | Compensation |
|---|---|
| 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#
| Column | Type | Notes |
|---|---|---|
order_id | UUID PK | |
store_id | UUID | |
customer_id | UUID | Guest UUID for guest checkouts |
guest_email | VARCHAR | Populated for guest checkouts only |
status | ENUM | See status transitions below |
source | ENUM | STOREFRONT / INTERNAL / EXTERNAL_CHANNEL |
shipping_addr | JSONB | Snapshot at placement |
billing_addr | JSONB | Snapshot at placement |
subtotal | NUMERIC | |
shipping_cost | NUMERIC | |
tax_amount | NUMERIC | |
total | NUMERIC | |
payment_ref | VARCHAR | paymentTransactionId from payment-service |
created_at | TIMESTAMPTZ | |
updated_at | TIMESTAMPTZ |
order_line_items#
| Column | Type | Notes |
|---|---|---|
line_item_id | UUID PK | |
order_id | UUID FK | |
variant_id | UUID | Reference only — no FK to catalog; catalog records can change |
quantity | INT | |
unit_price | NUMERIC | Locked at checkout; immutable after creation |
total_price | NUMERIC | |
snapshot | JSONB | {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 → DELIVEREDCancellation 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#
| Topic | Consumers |
|---|---|
order.placed | inventory-service (consume hard reservations), communication-service (confirmation email/SMS), marketing-service (loyalty points, attribution), BI |
order.confirmed | Fulfillment workflows |
order.shipped | communication-service (shipping notification), customer profile service |
order.delivered | communication-service (delivery notification), marketing-service (review request trigger) |
order.cancelled | inventory-service (release reservations if not yet consumed), payment-service (refund if charged), communication-service |
order.refunded | payment-service, BI |
GraphQL Subgraph#
order-service participates in Apollo Router federation as a subgraph.
Order — @key(fields: "id"). Fields: orderStatus, lineItems, total, createdAt.
| Query | Signature |
|---|---|
order | order(id: ID!): Order |
customerOrders | customerOrders(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 path | Purpose |
|---|---|
ss3/kv/order-service/db.url | PostgreSQL JDBC URL |
ss3/kv/order-service/db.username | Database username |
ss3/kv/order-service/db.password | Database password |
ss3/kv/shared/ | Shared Kafka bootstrap and OTel config |
Item Data Across Services#
| Service | Price Source | Price Freshness | Storage |
|---|---|---|---|
| Cart | Catalog gRPC (GetVariantPrices) at display time | Always live — fetched on every cart view | Redis per session |
| Checkout | Catalog gRPC (GetVariantPricesForCheckout) at checkout start | Always live — this is the authoritative price lock | Transient in memory |
| Order | Passed by checkout-service at order creation | Frozen at placement — never updated | PostgreSQL, immutable |