
Once your system crosses a process boundary, “a transaction” stops being a feature and becomes a strategy. This post is a practical mental model for distributed data: what to keep strongly consistent, what to make eventually consistent, and how to do it safely with outbox + sagas.
Axel Domingues
“Just wrap it in a transaction.”
That sentence works… right up until your system crosses a boundary you can’t BEGIN and COMMIT around:
At that point, the problem isn’t distributed systems theory.
It’s product correctness under failure.
This month is about building the mental model and the practical toolkit for that reality:
Not just patterns — a decision about what must never be wrong.Distributed data needs a truth design.
Distributed data is not a technology problem.
It’s an invariants problem.
An invariant is a statement your business can’t afford to violate, even briefly.
Examples:
Everything else is negotiable.
And that’s the core reframing:
The goal isn’t “strong consistency everywhere.”
The goal is “strong consistency where it matters, eventual consistency everywhere else.”
The trap
Modeling every update as “must be immediate + correct everywhere” pushes you into brittle distributed transactions.
The unlock
Define invariants, then design a flow where those invariants are protected by one authoritative writer.
When teams first hit multi-service updates, they reach for the database instinct:
Sometimes, with very controlled infrastructure, you can.
But in most modern systems, distributed transactions are a poor default because they:
It doesn’t fail every day. It fails on the day your business cares the most.
So what do we do instead?
We stop trying to make the whole world transactional.
We split the problem into:
That’s outbox + saga thinking.
A useful mental model is to separate three things:
What your database stores.
This is where you can be atomic.
What you want to happen next.
This must be durable, even if the system crashes immediately after writing it.
Things outside your DB:
These must be retryable and idempotent.
The outbox pattern is simple:

Because your atomic unit becomes:
“State update and durable intent recording happen together.”
If the service crashes after the transaction commits, the outbox record still exists.
No ghost events. No lost events.
Publishing to a queue inside the DB transaction seems reasonable until the queue call succeeds and the DB commit fails (or times out).
Now you published an event for state that never committed.
That’s how you get ghost jobs and phantom emails.
Most messaging systems are at-least-once in real operation.
Exactly-once is a special kind of expensive, fragile promise.
Design for duplicates:
Outbox needs lifecycle management:
An outbox reliably publishes intent.
But many business flows are multi-step and multi-owner:
You need a durable way to progress through steps and handle failure.
That’s a saga.
A saga is a workflow with:
There are two main styles:
Orchestration
A coordinator service owns the state machine and tells participants what to do next.
Choreography
Services react to events and emit events. The “flow” emerges from subscriptions.
My rule of thumb:
Until the flow changes. Then you discover you built a distributed state machine with no single place to debug it.
Teams often treat these as synonyms.
They’re not.
“Things will probably converge later.”
“Under retries and failures, the system converges to a correct business outcome, and we can prove it.”
That proof comes from engineering discipline:
Let’s design a flow with realistic failure behavior.
Charge at most once.
(Everything else we can repair.)
Order(PENDING_PAYMENT) and writes an outbox message AuthorizePayment(orderId, amount, idempotencyKey).PaymentAuthorized or PaymentFailedFAILED_PAYMENTShipmentScheduledCOMPLETEDThe key is not the happy path.
It’s how every step behaves under retries, duplicates, and timeouts.
You can copy patterns forever and still ship brittle systems if you miss the fundamentals.
Here are the four constraints I treat as mandatory.
Idempotency everywhere
Every handler must be safe to run twice. Duplicates are normal.
Durable state machines
Workflow state must survive crashes. If the process dies, the truth must remain.
Explicit timeouts
If you don’t define time, the network defines it for you (and you won’t like the result).
Poison pill containment
Failures that can’t be auto-retried must go somewhere (DLQ / dead-letter table) with visibility.
This isn’t a framework.
It’s a set of boring, repeatable mechanics.
Include:
If the transaction commits, you are guaranteed the intent exists.
The dispatcher:
Use one of:
(idempotency_key) in your write modelIf a message fails N times:
at-least-once delivery + idempotent handlers + dead-lettering = sanity.
The best architectural move is often not “saga everything.”
It’s choosing boundaries intentionally:
Here’s the decision heuristic I use:
Pick a single system of record for the invariant.
Everything else can become:
Users tolerate eventual updates if the product is honest:
Not everything needs real-time coupling.
For rare inconsistencies:
If you’re reviewing a “distributed data” design, ask these questions:
If the design can’t answer those, it’s not an architecture yet.
It’s hope.
Transactional Outbox (pattern)
A practical approach to “DB write + publish” without ghost events or lost messages.
Outbox solves one specific problem: reliable publication of intent.
Sagas solve a different problem: durable progression of multi-step workflows.
If your flow is one-and-done, outbox + idempotent consumers might be enough.
If your flow has multiple steps, branching, or compensations, you want saga thinking.
No.
Eventual consistency is a tradeoff.
It’s acceptable where:
For money, security, and irreversible actions, design stronger invariants.
Start with:
You can evolve into deeper workflows as the system earns it.
This month was about making state changes safe across boundaries.
Next month is about making APIs safe across time.
Because even if your data is eventually correct…
your clients will still break if your contracts aren’t.
API Evolution at Scale: Compatibility, Contracts, and Consumer-Driven Testing
APIs don’t fail because they’re slow — they fail because they change. This month is about designing contracts you can evolve, enforcing compatibility automatically, and scaling teams without “everyone upgrade on Tuesday.”
Security for Builders: Threat Modeling and Secure-by-Default Systems
Security isn’t a checklist you add at the end — it’s a set of architectural constraints. This month is about threat modeling that fits real teams, and defaults that prevent whole classes of incidents.