Trade-offs & Decisions
The key architectural decisions in an e-commerce platform, and the trade-offs that make each one non-obvious -- from search infrastructure and cache boundaries through checkout timing, reservation-vs-intent, consistency scoping, reserve/pay ordering, order/payment sequencing, saga coordination style, expiry mechanisms, inventory concurrency, event delivery, partitioning, flash-sale admission and monolith-vs-services.
Catalog Database vs. Search Index
Should product search run against the catalog database or a dedicated index? Both are viable at different scales.
| Catalog database | Dedicated search index | |
|---|---|---|
| Capability | Basic full-text matching is possible and adequate at small catalog scale | Relevance ranking, faceted filtering, typo tolerance, autocomplete |
| Scaling | Search load competes with transactional write load on the same store | Scales independently from the transactional catalog store |
| Consistency | Always as fresh as the catalog itself | Eventually consistent -- an indexing pipeline introduces lag |
| Operational cost | None beyond the existing database | A second system to run, monitor and keep in sync |
Synchronous vs. Asynchronous Checkout
It is not accurate that inventory reservation, payment, and order creation must all complete synchronously before responding to the customer -- some payment methods are inherently asynchronous.
A valid checkout response can legitimately be PENDING rather than CONFIRMED, when the chosen payment method resolves asynchronously (a bank redirect, a challenge flow). The real rule does not depend on synchronous vs. asynchronous plumbing at all:
What is a genuine design choice: downstream business effects (fulfillment kickoff, notification, analytics) should generally be event-driven off durable confirmed state, not tied to whether the original checkout call happened to be synchronous.
Cart vs. Inventory Reservation
Should adding an item to cart reserve inventory against it?
| Reserve on add-to-cart | Reserve at checkout (baseline) | |
|---|---|---|
| Behavior | Prevents another shopper from buying the item while it sits in someone's cart | Cart is intent only; reservation happens when the shopper commits to purchase |
| Downside | Abandoned carts can block inventory indefinitely; popular items can appear falsely sold out | A shopper can reach checkout and find the item just sold out -- needs a clear, informative error |
| When it's used | Extremely scarce, time-sensitive inventory (limited drops, ticketing) usually paired with a waiting room/queue | The default for ordinary retail with normal-velocity inventory |
Consistency by Workload
Different parts of the platform warrant different consistency treatment -- but the useful statement is a list of concrete invariants, not a label like 'strong consistency.'
| Eventual consistency is acceptable | A concrete guarded invariant is required |
|---|---|
| Catalog display, search results, recommendation updates, analytics aggregates, order history display | No duplicate checkout/order for the same idempotency identity |
| No inventory allocation beyond the policy the SKU actually permits | |
| Order state transitions only follow the guarded transition table | |
| A payment identity is never reused across two independent charges |
Reserve-Before-Pay vs. Pay-Before-Reserve
Should inventory be reserved before or after the payment attempt?
| Reserve, then pay | Pay, then reserve | |
|---|---|---|
| Benefit | Never charges a customer for an item that turns out to be unavailable | Avoids holding stock during a payment attempt that might fail or take a while |
| Risk | Holds stock during the payment window, even if payment ultimately fails | A successful charge can be followed by unavailable inventory, forcing a refund |
Order-Before-Payment vs. Order-After-Payment
Should a durable pending order exist before the payment attempt, or only after payment succeeds?
| Create pending order first | Create order only after payment succeeds | |
|---|---|---|
| Recoverability | A durable workflow identity exists from the start -- crash recovery has something to resume | A payment that succeeds with no matching order is a harder gap to recover from |
| Bookkeeping | Abandoned/failed checkouts leave PENDING or CANCELLED order records behind | Fewer abandoned order records, at the cost of a harder recovery path |
Orchestration vs. Choreography
Should the checkout saga be coordinated by one explicit owner, or emerge from services reacting to each other's events?
| Orchestration | Choreography | |
|---|---|---|
| Coordination | One workflow owner explicitly drives each step | Services react to events published by other services |
| Visibility | Easier to see and query the current state of any given checkout | State is implicit, spread across independent consumers |
| Coupling | More central coupling to the orchestrator | Looser coupling between individual services |
| Failure reasoning | Easier as the workflow grows -- one place to look | Harder to reason about end-to-end as more steps/services are added |
Reservation Expiry Mechanism
Several mechanisms can implement durable, recoverable reservation expiry -- they differ in operational cost and precision.
| Mechanism | Trade-off |
|---|---|
| Scheduled DB scan | Simple to build; scan cost grows with reservation volume unless well indexed |
| Delayed queue message | Precise timing; adds a dependency on a delay-capable queue/scheduler |
| Expiry index | Makes the scan option scale; still a polling-based mechanism, not instantaneous |
| Workflow engine timer | Precise and integrated with the saga's own state; only available if a workflow engine is already in use |
See Architecture Deep Dive -> Reservation Expiry for the full mechanics of each option.
Inventory Concurrency Strategy
Conditional atomic update, optimistic versioning, row locks, and serialized per-SKU processing all implement the same invariant -- the choice is about contention level, not correctness.
| Strategy | Best when |
|---|---|
| Conditional atomic update | Ordinary contention -- the default for most SKUs |
| Optimistic versioning | Moderate contention, retries are cheap |
| Row lock | Multiple related checks/writes need to happen inside one transaction |
| Serialized per-SKU processing | Extreme contention on a single hot SKU -- see Flash-Sale Admission Control below |
See Architecture Deep Dive -> Inventory Concurrency Control for the mechanics of each.
Direct Publish vs. Transactional Outbox
Publishing order.confirmed directly after the DB commit is simpler to build, but leaves a real dual-write gap that the outbox pattern exists to close.
| Direct publish | Transactional outbox | |
|---|---|---|
| Implementation cost | Lower -- one call to the broker after commit | Higher -- an outbox table, a relay process, relay-lag monitoring |
| Failure window | A crash between DB commit and the publish call silently drops the event | The event's existence is guaranteed once the DB transaction commits |
| Consumer requirement either way | Consumers must still be idempotent | Consumers must still be idempotent -- at-least-once, not exactly-once |
Inventory Partitioning
Should inventory be partitioned by SKU, by location, or by a broader database-wide key shared with other domains?
| SKU/location partitioning | Shared platform-wide key (e.g. customer_id) | |
|---|---|---|
| Contention isolation | Isolates contention to the specific SKU under load | A hot customer or shard key can bundle unrelated SKUs together |
| Query fit | Matches how reservation and stock queries are actually made | Convenient for cross-domain joins, poor fit for inventory's own access pattern |
SKU/location partitioning is the natural fit for inventory's access pattern. See Architecture Deep Dive -> Partitioning for how this compares across catalog, cart, inventory and orders, and Deep Dive -> Flash-Sale Hot-SKU Contention for why partitioning alone doesn't solve one indivisible hot SKU.
Flash-Sale Admission Control
Should every user be allowed to contend for a scarce SKU simultaneously, or should admission be smoothed?
| Let all users contend | Waiting room / token admission | |
|---|---|---|
| Simplicity | No additional component | Adds a queuing/admission layer |
| Contention | Every request piles onto the same hot reservation path at once | Admission is smoothed; the hot path sees bounded concurrency |
| Fairness | First-to-arrive-at-the-database wins, not necessarily first-to-click | Can implement an explicit, product-defined fairness policy |
Price at Cart vs. Checkout
Should the cart's stored display price be trusted, or should checkout always resolve a fresh quote?
| Trust the cart's stored price | Resolve a fresh checkout quote | |
|---|---|---|
| Risk | Charges a stale price the customer no longer owes or is no longer offered | None -- the customer is always quoted current commercial terms |
| UX cost | None until the stale price causes a dispute | May need to surface a price-changed notice if the quote differs from what was displayed |
Modular Monolith vs. Independently Deployed Services
The domain boundaries this module teaches -- catalog, pricing, cart, inventory, checkout, order, payment, fulfillment -- matter even before a single service is independently deployed.
A modular monolith that keeps these boundaries as clean internal modules (separate schemas/tables, separate transaction scopes, explicit interfaces between them) can satisfy every invariant in this module without operating a dozen independently deployed services. Splitting into separate services is justified by independent scaling needs, independent deployment cadence, or team ownership boundaries -- not assumed as a requirement of correctness.