In a system split across several services, how do you decide which service is the single source of truth for a given piece of data, and what goes wrong when two services both write it?
answer
- owner = who enforces the invariants
- per field, not always per entity
- two writers ⇒ lost update, no winner
- dual write ⇒ outbox or CDC
- readers: sync call, or event-fed read model
basics
~20 sGive each fact to exactly one service — the one that owns the business rules for changing it. Others read it or subscribe to its change events; they never write it. Two writers means conflicting values with no rule for which is correct.
solid answer
~50 sAssign ownership by *who owns the invariants*: the service that enforces the rules governing a fact should own its storage and be its only writer. Everyone else gets a read path — a synchronous query, or a local read model kept up to date from the owner's change events. Ownership is per *field* where necessary, not always per entity: billing may own the billing address while the profile service owns the display name. Two writers produce **conflicting writes** with no deterministic winner: last-write-wins silently discards data, clock skew makes 'last' ambiguous, and the two stores can end up with values neither service ever intended. A related failure is the **dual write** — one service writing to its database *and* publishing an event (or writing a second store) without a transaction spanning both, so a crash between them leaves the copy permanently wrong; the standard fixes are the transactional **outbox** pattern or change-data-capture, which make the event a consequence of the committed write rather than a second, independent write.
go deeper
Say each piece of data should belong to one service that writes it, and other services read it or listen for changes; two writers means conflicting values.
Choose the owner by who originates and validates the data; describe the reader options (sync call vs local read model from events) and the staleness trade-off.
Lead with invariant ownership, discuss per-field ownership across bounded contexts, name lost updates and last-write-wins ambiguity, and specify outbox/CDC to avoid dual writes.
Treat ownership as an organizational contract with a catalogue and review; define policy for migration windows with two writers, multi-region/offline cases needing designed conflict resolution (CRDTs, version vectors), and sagas for cross-owner operations.
## Terms - **Owner**: the single service allowed to write a fact and responsible for its invariants. - **Invariant**: a rule that must always hold (an order cannot ship before payment clears; an email must be unique). - **Read model / local cache**: another service's copy of the owner's data, kept fresh from events, used to avoid a synchronous call. - **Dual write**: writing the same logical change to two systems without a shared transaction (DB + message broker, DB + search index). - **Outbox pattern**: within the same database transaction as the state change, insert a row into an `outbox` table; a separate relay reads that table and publishes to the broker. The write and the intent-to-publish commit atomically. - **CDC (change data capture)**: reading the database's own commit log to emit change events, giving the same atomicity property without an outbox table. ## Choosing the owner — practical criteria, in order 1. **Who enforces the invariants?** If a rule must be checked on every change, the checker must own the data; otherwise the rule can be bypassed. This is the strongest signal. 2. **Who is the system of record for the business?** Follow the bounded context that the domain experts treat as authoritative for that concept. 3. **Who writes it most / originates it?** The producer of the data is usually the natural owner; readers are cheap to serve, writers are not. 4. **Lifecycle alignment**: the fact should live with the entity whose lifecycle it follows (created, changed and deleted together). 5. **Blast radius**: prefer the owner whose availability profile matches how critical the write path is. Go **per field** when an entity legitimately spans contexts: 'customer' in CRM, billing and support are usually three different aggregates that share an identifier, not one record with three writers. Model them as separate aggregates linked by ID, each owning its own fields, rather than fighting over one shared row. ## What actually goes wrong with two writers - **Lost update**: A reads value, B reads value, both write; one change vanishes. No error is raised. - **No deterministic winner**: last-write-wins depends on clocks; skew across hosts makes 'last' arbitrary. Logical clocks or version vectors can order writes but still require a merge policy. - **Split invariants**: each writer enforces its own rules, so the combined state can violate rules both thought they were protecting (two services each ensure at most one active subscription; together there are two). - **Unfixable reconciliation**: with one owner, repair means re-derive. With two owners, repair requires a *business decision* per record about which value was intended. - **Diffusion of responsibility**: on-call cannot answer "who is wrong" because both stores are equally authoritative. ## Giving readers what they need without a second writer | Approach | Freshness | Coupling | Failure mode | |---|---|---|---| | Synchronous query to owner | current | runtime dependency on owner's availability and latency | owner down ⇒ caller degraded | | Local read model from events | eventually consistent | build-time contract only | stale reads; needs replay + idempotency | | Cached copy with TTL | bounded staleness | low | stale within TTL window | | Data replication / shared DB | current | very high — schema becomes a shared contract | any schema change breaks consumers | A reader that must *decide* something on stale data needs care: either accept the staleness explicitly (and design compensating actions), or make the owner the decision point via a synchronous call or a request/reply. The common pattern is: read models for display and filtering, owner-side checks for anything that enforces an invariant. ## Dual write vs outbox — why it matters here Even with a single owner, the owner often must update its own DB and tell the world. Naively: ``` db.commit(change) broker.publish(event) // crash here ⇒ event never sent, copies drift forever ``` or the reverse order (event sent, DB rolled back ⇒ copies show a change that never happened). Neither ordering is safe because there is no transaction across the two systems. The fixes: - **Outbox**: `db.transaction { applyChange(); insertOutbox(event) }`, then a relay publishes and marks sent. Delivery is at-least-once, so consumers must be idempotent. - **CDC**: tail the commit log; events are a function of committed state by construction. - **Listen-to-yourself**: publish first to a durable log, then have the owner consume its own event to update state — makes the log the source of truth (event sourcing territory). ## Edge cases and honest exceptions - **Cross-service transactions** are not available; multi-step business operations use **sagas** with compensating actions, keeping each step within one owner. - **Offline/multi-region writers** genuinely cannot have one live owner; then you accept multiple writers and adopt an explicit conflict resolution model — CRDTs, version vectors with a merge function, or human review queues. The point is that the merge rule is *designed*, not accidental. - **Migration windows** temporarily have two writers; make it explicit, time-boxed, one-directional if possible (dual-write with a designated authority and a continuous diff), and delete the old writer on a date. - **Reference data** (country codes, tax tables) is read-mostly and can be replicated everywhere freely — one owner publishes versioned snapshots.
- Two teams insist they both need to write the customer's address. How do you resolve it?Split the fact: they almost certainly mean different addresses (billing vs shipping vs contact) with different lifecycles and rules. Model them as separate fields owned by separate aggregates linked by customer ID. If it really is one fact, pick the owner by who enforces its validation rules and give the other a write API on the owner plus an event feed back.
- Why is the transactional outbox preferred over publishing the event right after the commit?Because the commit and the publish are not atomic: a crash between them loses the event permanently and every downstream copy drifts with no signal. The outbox writes the event row inside the same transaction as the state change, so either both happen or neither; a relay then publishes at-least-once, which idempotent consumers handle.
A shared document where two people both have edit rights and no merge tool: whoever saves last wins, and neither knows what the other removed. Give one person write access and everyone else comment/read access, and the document always has a defensible state.