E-commerce Platform
Catalog, inventory, checkout, fulfillment, and returns without overselling.
Product overview
Sell physical goods from a searchable catalog while keeping prices, stock, money, fulfillment, and returns correct across retries and partial failures.
Main actors
Customer, Merchant, Warehouse operator, Support.
Assumptions
- One legal seller of record in v0.1
- Inventory is stocked at one or more fulfillment locations
- A PCI-compliant provider hosts card collection
- Example workload: 25 orders/second at peak, not a benchmark
Non-goals
- Marketplace seller onboarding
- Warehouse robotics
- Building a payment processor
- Globally active-active checkout
Requirements
Must-have
- Browse and search purchasable products
- Maintain server-authoritative carts
- Reserve inventory during checkout
- Authorize payment and create orders idempotently
- Track shipments and process refunds
Should-have
- Guest checkout
- Promotion rules with an explainable price breakdown
- Split fulfillment by location
- Customer service order timeline
Future scope
- Marketplace sellers
- Subscriptions
- Cross-border duties
- Personalized ranking
Non-functional decisions
| Concern | Decision |
|---|---|
| Availability | Browsing may degrade to cached catalog data; checkout fails closed if price, inventory, or payment dependencies are uncertain. |
| Consistency | Order totals and stock reservations are transactional. Search, recommendations, and shipment tracking may lag. |
| Durability | Acknowledged orders and outbox records share one database commit; payment state is reconciled with the provider. |
| Privacy | Minimize stored addresses, encrypt sensitive fields, and apply retention to guest data. |
| Latency | Keep catalog reads cacheable; checkout is slower but bounded by provider timeouts and explicit pending states. |
User roles
| Role | Access boundary |
|---|---|
| Customer | Own carts, orders, addresses, and return requests |
| Catalog manager | Products and prices; no payment or support impersonation |
| Warehouse operator | Assigned fulfillment tasks and stock adjustments |
| Support | Scoped order view and audited refund request, never raw payment credentials |
Core workflows
Domain model
| Entities | Relationship |
|---|---|
Product → Variant | A product presents one or more sellable SKUs |
Cart → CartLine | A cart snapshots quantity, not authoritative price |
Order → OrderLine | Immutable commercial snapshot of SKU, price, tax, and discount |
InventoryItem → Reservation | On-hand stock is protected by expiring reservations |
Order → Shipment / Return | An order can split into shipments and later line-level returns |
Database design
PostgreSQL remains the transactional source of truth. Add specialized stores only for the workloads called out below.
product_variants — Sellable SKU and catalog state
inventory — Stock by SKU and location
orders — Checkout aggregate and payment state
order_lines — Immutable purchase snapshot
inventory_reservations — Expiring claim on stock
Transaction and retention boundaries
- Lock relevant inventory rows in deterministic SKU order, verify available = on_hand - reserved, increment reserved, insert reservations, order, and outbox atomically
- Refund records and order refundable amount update share a transaction; provider calls happen outside it
- Hard-delete abandoned carts after retention; never soft-delete commercial order records—use explicit states
API design
| Endpoint | Purpose and permission | Idempotency / status |
|---|---|---|
GET /v1/products?cursor=… | Browse catalog using opaque stable cursor Public | Not required 200, 400 |
POST /v1/carts/:id/lines | Add or replace a cart line Cart owner | Not required 200, 409 |
POST /v1/orders | Price cart, reserve inventory, and start checkout Cart owner | Required; key scoped to account and payload hash 201, 402, 409, 422 |
GET /v1/orders/:id | Read order timeline Order owner or scoped support role | Not required 200, 403, 404 |
POST /v1/orders/:id/cancel | Request state-aware cancellation Owner or support | Required 202, 409 |
POST /v1/returns | Create return authorization for eligible lines Order owner or support | Required 201, 409, 422 |
List endpoints use opaque keyset cursors unless a small bounded collection is explicit. Unknown objects return 404 where 403 would leak existence.
Architecture
System context
Main application architecture
Critical workflow sequence
Event model
| Event | Producer → consumers | Delivery |
|---|---|---|
OrderConfirmed | Checkout → Fulfillment, email, analytics | Transactional outbox; at-least-once; consumers deduplicate by event id |
ReservationExpired | Inventory worker → Order workflow | Retry safely; conditional status transition prevents releasing twice |
ShipmentDispatched | Fulfillment → Notifications, order projection | Dead-letter after bounded retries; operations can replay |
Failure modes
Stock race
Failure
Two checkouts see the last item.
Mitigation
Row lock and invariant check inside one reservation transaction; loser receives 409.
Payment timeout
Failure
Provider authorizes but the API sees a timeout.
Mitigation
Keep order payment_pending; query by provider idempotency reference and reconcile before retrying.
Duplicate submit
Failure
Client retries POST /orders.
Mitigation
Persist idempotency key, payload hash, status, and prior response under the same account.
Event redelivery
Failure
Fulfillment receives OrderConfirmed twice.
Mitigation
Inbox/event receipt unique constraint and idempotent task creation.
Reservation leak
Failure
Worker crashes after payment failure.
Mitigation
Expiry-indexed reaper releases only active reservations with conditional updates.
Stale catalog
Failure
Cached page shows an old price.
Mitigation
Display indicative price; reprice server-side and require explicit confirmation when total changes.
Consistency and concurrency
Strong consistency / transactions
- Inventory reservation
- Order total snapshot
- Refundable balance
- State transition preconditions
Eventual convergence
- Search index
- Recommendations
- Carrier tracking projection
Concurrency controls
- Use SELECT … FOR UPDATE on inventory rows, not a distributed lock
- A unique idempotency record prevents duplicate orders
- Order version or conditional status UPDATE rejects cancellation after dispatch
- Refund sum is checked while locking the order payment row
Caching and background work
Safe to cache
- Published catalog by locale
- Product media
- Facets and popular search results
Never a cache authority
- Available-to-promise during checkout
- Authoritative price at order creation
- Refundable amount
Cache-aside with short TTL plus product-change invalidation; CDN assets use content hashes. Serve stale catalog on read failure but never bypass checkout validation.
Background jobs
- Expire stock reservations
- Publish transactional outbox
- Index product changes
- Send receipts
- Poll carrier status
- Reconcile payment intents and refunds
Security boundaries
Security claims here are design controls, not certification. Threat modeling and jurisdiction-specific review remain required.
- Object-level checks on every cart, order, address, and return
- Hosted payment fields keep card data outside the application boundary
- Signed provider webhooks with timestamp tolerance and replay deduplication
- Promotion and quantity inputs revalidated server-side
- Rate limits by account, IP, and checkout risk signals
- Audit privileged refunds and inventory adjustments with reason
- Prevent stored XSS in merchant-authored catalog content
Observability
Logs
- Order, reservation, payment, and fulfillment transitions with request/order ids and reason—never addresses or payment tokens
- Checkout repricing differences and rejected stock invariants
- Outbox/worker attempt with event id and terminal outcome
Metrics
- Checkout authorization failures and payment_pending age
- Inventory reservation conflict and expiry rate by SKU/location
- Order-to-allocation and allocation-to-dispatch latency
- Outbox age, fulfillment backlog, and refund reconciliation exceptions
Traces
- Trace checkout across pricing, inventory transaction, provider authorization, outbox, and fulfillment
- Trace return receipt through refund initiation and provider convergence
Alerts
- Confirmed orders without fulfillment work
- Payment-pending orders exceed reconciliation window
- Reserved exceeds on-hand invariant or reservation expiry worker stalls
Testing strategy
| Layer | What it proves |
|---|---|
| Unit | Money arithmetic, promotion precedence, state machines, reservation expiry |
| Integration | Real PostgreSQL constraints, row locking, outbox atomicity, provider adapter sandbox |
| Contract | Payment and carrier webhook fixtures, consumer event schemas |
| Concurrency | Many checkouts for one SKU; assert reserved never exceeds on_hand |
| Failure | Timeout after authorization, duplicate events, worker death before acknowledgement |
| End-to-end/load | Browse → checkout → cancel/ship/return; workload shaped by browse-to-buy ratio |
Do not mock PostgreSQL constraints, transaction isolation, object-store semantics, or provider contract fixtures in the tests intended to prove those boundaries.
Deployment evolution
Scale evolution
These are architecture scenarios, not throughput claims. Each transition should follow measured workload and reliability pressure.
| Scenario | Architecture may evolve toward | Decision rule |
|---|---|---|
| 1K users | One API and worker, PostgreSQL for orders and stock, managed object storage for product images | Keep stock reservation and order creation in one PostgreSQL transaction; measure reservation lock waits before adding infrastructure |
| 100K users | API replicas; Redis for catalog; search service; worker pools; database read replicas while writes remain primary-owned | Add Redis and search when catalog reads dominate; replicas never answer stock questions during checkout |
| 1M+ users | Regional catalog edges and partitioned order/stock ownership; separate fulfillment and search workloads; never multi-writer inventory without an explicit conflict model | Split stock ownership by warehouse or region only after hot-SKU lock contention is measured; each SKU keeps one writer |