Architecture Decision Pack · USD 79
ADR-014: how orders and inventory talk to each other
Three options on the table, two of which are the same option. The decision, the trade-offs, and what it costs to be wrong.
The team has been arguing about this for five weeks. Reading the three proposals, two of them are the same architecture described in different vocabulary, which is why the argument has not converged. There are two real options, not three.
Proposal A (shared database) and Proposal C (inventory reads the orders table directly) are the same decision: orders owns a table that inventory couples to. Only Proposal B is different. On your constraints, and specifically the requirement that inventory stay available when orders is down, B is the one that meets them.
Context
The problem, stated once
Orders and Inventory were one service until March. They are now two, deployed separately, owned by two pairs. They still share a database. Every schema change requires both teams, deploys are coupled, and the split has delivered none of the independence it was done for.
| Constraint | Value | Source |
|---|---|---|
| Inventory availability target | 99.5 percent monthly | Customer contract, not an internal goal |
| Orders availability target | 99.9 percent monthly | Customer contract |
| Acceptable stock staleness | Up to 30 seconds | Agreed with the commercial team, 24 September |
| Peak order rate | 140 per minute, Black Friday 2025 | Observed |
| Team size | Six engineers, two pairs plus a lead | Given |
| Existing message infrastructure | None. Nothing deployed today | Matters, see clause 4 |
| Deadline | Decision by 10 October, build in Q4 | Given |
The 30 second staleness figure is the most important line here and it did not exist before this engagement. The argument ran for five weeks partly because nobody had asked the commercial team how fresh stock counts need to be.
Options
The two real options
Option 1: Orders owns the table, inventory reads it
Whether inventory reads through a shared database or through a synchronous API to orders, the coupling is the same: inventory cannot serve a request when orders is unavailable, and an orders schema change can break inventory. Cheapest to build, and it keeps the current failure mode.
Option 2: Orders publishes events, inventory keeps its own view
Orders emits an event on every state change. Inventory consumes it and maintains its own stock projection. Inventory serves reads from its own store and stays up when orders is down. Costs a message broker you do not currently run, and makes stock eventually consistent within the agreed 30 seconds.
| Option 1 | Option 2 | |
|---|---|---|
| Inventory survives orders outage | No | Yes |
| Meets 99.5 percent inventory target | Not independently | Yes |
| Stock is immediately consistent | Yes | Within 30 seconds |
| Teams deploy independently | No | Yes |
| New infrastructure to run | None | A broker |
| Build effort | About 2 weeks | About 6 weeks |
| Operational burden added | None | Real, see clause 4 |
| Reversible | Yes, it is today's state | Hard after 6 months |
Contract
The interface, if Option 2 is chosen
Orders owns the event. Inventory owns its projection. Neither reads the other's database. The contract is the event, and it is versioned.
order.line_reserved, v1
{
"event": "order.line_reserved",
"version": 1,
"event_id": "01J8X2QKQ4RN7WZ3M1",
"occurred_at": "2026-10-01T09:14:22.118Z",
"order_id": "ord_8812",
"line_id": "ln_4471",
"sku": "TH-0912-BLK",
"quantity": 2,
"location_id": "wh_leeds",
"idempotency_key": "ord_8812:ln_4471:reserved:1"
} | Field | Owned by | Rule |
|---|---|---|
| event, version | Orders | Additive changes only within a version. A breaking change is version 2, published alongside v1 for 90 days |
| event_id | Orders | Unique. Consumers deduplicate on it |
| idempotency_key | Orders | Stable across retries. This is what makes redelivery safe |
| sku, location_id | Shared vocabulary | Neither service may invent one. Both come from the product catalogue |
| quantity | Orders | Always positive. A release is a separate event, never a negative quantity |
| Stock level | Inventory, exclusively | Orders never asserts a stock level. It asserts what it reserved |
The last row is the whole point of the decision. Today orders both reserves stock and knows the stock level, which is why the two services cannot be separated.
Costs
What Option 2 actually costs you
The build estimate is the easy part. These are the things that are usually left out of the estimate and then arrive anyway.
| Cost | Detail | Who carries it |
|---|---|---|
| Running a broker | Nobody on the team has operated one in production | Platform pair |
| Dead letter handling | Events that fail repeatedly need somewhere to go and somebody to look | Both pairs |
| Replay capability | When the projection is wrong, you rebuild it from the log. This has to be built, not assumed | Inventory pair |
| Monitoring consumer lag | The failure mode becomes stale data rather than an error, which is harder to notice | Platform pair |
| Support and commercial | Support has to understand that stock can be 30 seconds behind | Operations |
| Reversal difficulty | After six months of events, going back to a shared database is a migration | The company |
Open
Four questions the team must answer
- Which broker, and who operates it? The decision between Option 1 and Option 2 does not depend on the answer, but the six week estimate does.
- What happens to a reservation if the event is never consumed? There must be a timeout, and the commercial team has to set it.
- Does the 30 second staleness hold on Black Friday, at 140 orders a minute? The agreement was given for a normal day.
- Who owns the shared SKU vocabulary? Neither service should, and today neither does.