Good morning, builders of the invisible plumbing that keeps the internet from catching fire.
If you have ever watched two microservices argue over whether an order was actually placed, welcome home. If you have ever lain awake wondering whether your message bus silently dropped an event while your database happily committed a row, pull up a chair. And if you have ever confidently told a product manager, “Sure, we can make that eventual-consistent,” while internally praying to the gods of idempotency, then this issue of The Backend Developers is written specifically for you.
Today we are talking about event-driven architecture in 2026, and the three horsemen that make it tolerable: outboxes, saga patterns, and the increasingly nuanced world of distributed consistency. The research is clear, the vendors are merging, the Python ecosystem is still a little scrappy, and the real differentiator is no longer which tool you pick but how well you assemble the layers.
So grab your coffee, close Slack for twenty minutes, and let’s build something worth reading.
Why Distributed Consistency Still Feels Like Herding Schrödinger’s Cats
Let’s set the stage. Ten years ago, the world discovered microservices and immediately made two catastrophic assumptions. First, that breaking a monolith into smaller pieces automatically makes you agile. Second, that you could still pretend you had one big happy ACID transaction when, in reality, you had just distributed your database across a hundred services, a Kafka cluster, three clouds, and one engineer’s laptop.
The truth is that most real-world business processes cross service boundaries. An e-commerce checkout flow touches inventory, payment, shipping, notifications, loyalty points, fraud checks, and tax calculators. None of these services share a database. You cannot wrap the whole thing in BEGIN TRANSACTION; COMMIT;. Distributed transactions through two-phase commit are theoretically beautiful and operationally haunted. They lock resources, slow systems to a crawl, and turn partition tolerance into an existential crisis.
So we turned to event-driven architecture. Services publish facts. Other services react to those facts. The system becomes loosely coupled, horizontally scalable, and theoretically resilient. The catch? Publishing an event and committing a database row are two separate operations, and networks are not your friend.
If you commit the database first and then publish the event, a broker hiccup means the database says “yes” while the rest of the system says “never heard of it.” If you publish the event first and then commit the database, you get ghost orders haunting Kafka. Either way, someone is angry, and it is probably the finance team.
That is where the outbox pattern enters. Then, when the business process itself spans multiple services and failures can happen halfway through, the saga pattern enters. And when people start asking whether their system is eventually, causally, or linearizably consistent, you enter the consistency-model negotiation that determines whether your architecture survives contact with real customers.
Let’s take each of these seriously, because beneath the buzzwords are genuinely powerful ideas.
The Outbox Pattern: Your Atomic Bridge Between State and Events
Here is the core problem in plain language. In a microservice, you typically want to do two things when something important happens:
Mutate your own local state in a database.
Tell the rest of the world about it through a message broker or event bus.
These two actions must appear atomic to the rest of the system. Either the state change and the event both happen, or neither happens. But unless your database and your broker share a transaction coordinator, which they almost never do, you cannot wrap both in a single transaction.
The outbox pattern solves this by making the event part of the same database transaction as the state change.
Instead of directly publishing to the broker inside your service code, you append a row to an outbox table within the same local database transaction that updates your domain state. Because the event row lives in the same database as your business data, the database’s transaction guarantees apply to both. If the transaction commits, the state change and the event row are durably persisted together. If the transaction rolls back, neither survives.
A separate component, often called the outbox relay or event relay, then reads committed events from the outbox table and publishes them to the message broker. This can be done by polling the table or, more elegantly in modern systems, by using change-data capture via tools like Debezium that tail the database’s write-ahead log. Either way, once an event is successfully acknowledged by the broker, the relay marks it as processed or deletes it.
This gives you several critical properties.
First, atomicity. Domain state changes and event publication are now one logical unit. There is no window where one happened and the other did not, at least from the perspective of durability.
Second, at-least-once delivery. The relay will keep trying to publish an event until it receives confirmation from the broker. The event is not lost because the database is the source of truth. The broker may receive the event more than once due to retries, duplicates, or network quirks, but the source event itself is preserved.
Third, per-aggregate ordering. Because the outbox table is written inside the service’s database transaction, events for a given aggregate can be written in the exact order the aggregate changed. The relay can then publish them in that order, often keyed by aggregate ID so that consumers receiving from a partitioned topic see a coherent history for each aggregate. If aggregate A changes twice, consumers will see event A1 before event A2 as long as the relay respects the ordering.
Fourth, a pathway to exactly-once effects. The outbox pattern alone does not give you end-to-end exactly-once delivery semantics in a strict distributed systems sense. Message brokers typically provide at-least-once delivery. True exactly-once processing requires the consumer to be idempotent: processing the same event twice must produce the same result as processing it once. The outbox makes the publisher side safe, and idempotency makes the consumer side safe. Together they produce an exactly-once effect.
There are practical nuances to consider. The outbox table should ideally be co-located in the same database schema as the domain tables to avoid cross-database transactions. The relay should publish events with a stable, unique identifier so consumers can deduplicate. The table will grow unless events are removed or moved to an archive after successful publication, so operational habits matter. Polling at very high throughput can become inefficient, which is why Debezium-style CDC has become popular: it reads the database’s transaction log directly, minimizing load on the service database and reducing latency.
The outbox pattern is not glamorous. It is just a table and a loop. But it is the canonical answer to one of the hardest problems in event-driven systems: making sure that when your service says something happened, it actually happened.
Saga Patterns: Orchestration, Choreography, and the Art of Saying “Oops”
Once you have atomic state-and-event publication, you face the next problem: many business processes are long-running and span multiple services. A single logical operation, like placing an order, consists of several local steps, each with its own database and its own events. If step three fails, what do you do about steps one and two?
You cannot roll them back with a single distributed transaction. You can, however, run compensating transactions: operations that semantically undo earlier steps. This collection of steps and compensations is called a saga.
A saga is a sequence of local transactions. Each step updates data in one service and emits a message or event that triggers the next step. If a step fails, the saga executes compensating transactions for the steps that have already completed. The goal is to leave the system in a consistent, well-defined state: either the whole process succeeded, or the saga has fully compensated and left a clear audit trail.
There are two dominant saga styles, and the choice between them is one of the most important architectural decisions you will make.
Choreography means there is no central coordinator. Each service knows which events to listen for and which events to emit next. When the inventory service receives an OrderCreated event, it reserves inventory and emits an InventoryReserved event. The payment service listens for that and charges the customer, emitting a PaymentCharged event. The shipping service listens for that and prepares delivery.
The benefit is loose coupling. Services do not need to know about each other explicitly, only about the events. This can feel natural for simple, event-native domains. The drawback is implicit coordination. The business flow is encoded in a web of subscriptions and handlers, making it hard to visualize, debug, and modify. Compensation logic ends up scattered across multiple services, and reasoning about failure paths becomes an archaeological expedition through repositories.
Orchestration means there is a central saga coordinator that explicitly invokes each step and handles failures. The coordinator might be a dedicated workflow engine such as Temporal, Orkes Conductor, or a custom state machine. It knows the sequence, calls services, waits for responses, retries transient failures, and invokes compensating actions when necessary.
The benefit is visibility and control. You can inspect the state of a saga at any moment. You can enforce ordering precisely. You can centralize compensation logic or at least track it. You can retry, pause, and escalate. The drawback is that the orchestrator becomes a coupling point and a potential single point of failure, though modern durable workflow engines are designed to be highly available themselves.
In 2026, the research consensus is clear: choreography is elegant for simple flows; orchestration is essential for complex flows; hybrid approaches are increasingly common. A hybrid design might use choreography for naturally decoupled subdomains and introduce an orchestrator for the parts of the flow where sequencing, visibility, and compensation really matter.
Compensations deserve special attention. A compensating transaction is not a database rollback. It is a business operation that semantically undoes a prior step. If you reserved inventory, the compensation releases it. If you charged a customer, the compensation issues a refund. Compensations can themselves fail, so they must be idempotent and retryable. Some compensations may require human intervention after automated retries are exhausted. Designing good compensations is where architecture meets domain modeling: you must deeply understand what “undo” means for each business action.
The saga invariant is straightforward to state and hard to guarantee: every saga eventually reaches either a completed state or a fully compensated state. Achieving this requires idempotency, durable execution, observability, and clear escalation paths. The pattern does not eliminate failure. It makes failure a first-class citizen of your design.
Picking a Consistency Model Without Starting a Religious War
The word “consistency” is dangerously overloaded. In distributed systems it means at least three different things depending on who is talking. Let’s be precise, because the research is emphatic: there is no universal correct consistency model. You choose based on the invariants your business actually needs.
Eventual consistency means that if no new updates are made, eventually all replicas will converge to the same value. It is the workhorse of distributed systems because it allows high availability and partition tolerance. For many backend operations, eventual consistency is perfectly fine. If a search index lags your primary database by a few seconds, users rarely notice. If two microservices briefly disagree about a loyalty point balance, the world keeps turning.
However, eventual consistency does not give you any guarantees about ordering. Event A might logically cause event B, but a consumer could see B before A, or see them out of order relative to other aggregates. That matters when business invariants depend on causality.
Causal consistency preserves happens-before relationships. If process P reads a value written by Q, then P’s subsequent writes are seen after Q’s write by any process that observes P. In event-driven systems, causal consistency often appears through event sourcing and per-aggregate ordering. By routing all events for a given aggregate through the same Kafka partition, or by using logical clocks and vector clocks in collaborative systems, you can guarantee that consumers observe the causal history of that aggregate correctly. This is especially valuable for collaborative workloads, event-sourced domains, and workflows where steps have clear dependencies.
Stronger models, such as linearizability or serializability, guarantee that operations appear to execute in some total order, instantaneously at a single point in time. These are powerful but expensive. They typically require coordination, locks, consensus protocols, or distributed transaction managers. They reduce availability, increase latency, and complicate operations. They should be reserved for invariants that truly require them, such as preventing double-spending, ensuring inventory does not go negative, or enforcing unique global identifiers.
The right approach is invariant-driven consistency. For each business rule, ask: what would break if this were only eventually consistent? What would break if causality were violated? If the answer is “nothing important,” eventual consistency is your friend. If the answer is “the user experience becomes confusing,” causal consistency is probably worth the cost. If the answer is “we lose money or violate regulation,” then you need a stronger model, and you should isolate that specific invariant rather than forcing the entire architecture into a straitjacket.
In practice, event-driven systems in 2026 often mix these models. A checkout saga may use causal ordering within an aggregate, eventual consistency for analytics and search, and a strongly consistent reservation or ledger for the actual money movement. The art is not choosing the strongest model everywhere. The art is choosing the weakest model that preserves each invariant.
Python in 2026: A Pragmatic, If Slightly Duct-Taped, Toolkit
Let’s address the elephant in the server room. If you are a Python backend engineer, you have probably noticed that the Java and .NET ecosystems have shiny, battle-tested saga and outbox libraries while Python sometimes feels like you are building a spaceship out of pip-installable cardboard.
The research confirms this directly: Python tooling for sagas and outboxes is less mature than Java or .NET alternatives. There is no single dominant saga framework that everyone reaches for. The typical 2026 Python implementation is assembled from a few well-known pieces.
For asynchronous task execution, Celery remains the default choice, often backed by Redis or RabbitMQ. For event streaming, Kafka is common, accessed through libraries like confluent-kafka, kafka-python, or aiokafka for async workloads. For change-data capture from the outbox table, Debezium is the standard connector, tailing PostgreSQL or MySQL write-ahead logs and pushing changes into Kafka. For the actual service layer, FastAPI, Django, or Flask with SQLAlchemy or an async ORM handle the domain logic and outbox writes.
For orchestration, Python developers are increasingly reaching for the Temporal Python SDK or clients for Orkes Conductor, though the most mature Temporal documentation and use cases still lean heavily toward Go and Java. Some teams build lightweight saga coordinators in Celery or Dramatiq, storing saga state in PostgreSQL and manually wiring compensations. It is pragmatic, it works, and it requires discipline.
That pragmatism is not a weakness. It reflects Python’s strength as a glue language and the reality that many Python backend teams operate polyglot environments: Python for the application, Kafka and Debezium for the event backbone, Temporal or Conductor for durable orchestration. You do not need a single Python library that does everything. You need a coherent design that uses the right tool for each layer.
Building the Code: A Mini Order Flow with Outbox, Saga, and Idempotency
Let’s make this concrete. Imagine a simplified e-commerce flow:
Create an order.
Reserve inventory.
Charge payment.
Mark the order as confirmed and notify shipping.
If payment fails, we compensate by releasing the inventory. If inventory reservation fails, there is nothing to compensate yet. This is the kind of long-running process where an outbox gives us atomic domain events and a saga gives us safe failure handling.
Below is a Python sketch using SQLAlchemy for the outbox and domain state, Celery for asynchronous saga steps and compensation, and a simple in-memory-style approach to idempotency. This is not production-ready copy-paste code; it is a teaching scaffold. Real production code needs retries, deadlines, observability, and careful error handling, but the structure is what matters.
First, the domain and outbox models:
import uuid
import datetime
from sqlalchemy import create_engine, Column, String, Integer, DateTime, JSON, Boolean
from sqlalchemy.orm import declarative_base, sessionmaker
Base = declarative_base()
class Order(Base):
__tablename__ = "orders"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
status = Column(String, default="pending")
inventory_reserved = Column(Boolean, default=False)
payment_charged = Column(Boolean, default=False)
class OutboxEvent(Base):
__tablename__ = "outbox"
id = Column(Integer, primary_key=True, autoincrement=True)
aggregate_id = Column(String, index=True)
aggregate_type = Column(String)
event_type = Column(String)
payload = Column(JSON)
created_at = Column(DateTime, default=datetime.datetime.utcnow)
published = Column(Boolean, default=False)When the user places an order, we commit both the Order row and an OrderCreated outbox event inside the same transaction:
def create_order(session, customer_id, items, total_cents):
order = Order(status="pending")
session.add(order)
session.flush() # Generate order.id without committing
event = OutboxEvent(
aggregate_id=order.id,
aggregate_type="order",
event_type="OrderCreated",
payload={
"customer_id": customer_id,
"items": items,
"total_cents": total_cents,
},
)
session.add(event)
session.commit()
return order.idThis is the heart of the outbox pattern. The Order row and the OutboxEvent row are written atomically. Later, a relay publishes the event to Kafka. If Debezium is in the picture, it tails the WAL and streams the new OutboxEvent row directly, avoiding polling load.
The saga itself is a Celery workflow that knows the sequence and the compensations:
from celery import Celery
app = Celery("order_saga", broker="redis://localhost:6379/0")
SAGA_STEPS = [
("reserve_inventory", reserve_inventory_task, release_inventory_task),
("charge_payment", charge_payment_task, refund_payment_task),
("notify_shipping", notify_shipping_task, None),
]The orchestrator keeps saga state and walks through the steps:
@app.task(bind=True, max_retries=3)
def run_order_saga(self, order_id):
saga = get_or_create_saga_state(order_id)
for idx in range(saga.current_step, len(SAGA_STEPS)):
step_name, action, compensation = SAGA_STEPS[idx]
try:
result = action.delay(order_id).get(timeout=10)
saga.results[step_name] = result
saga.current_step += 1
save_saga_state(saga)
except Exception as exc:
# Failure: compensate already-completed steps in reverse order.
for prev_idx in reversed(range(idx)):
prev_name, _, prev_comp = SAGA_STEPS[prev_idx]
if prev_comp:
prev_comp.delay(
order_id, saga.results.get(prev_name)
).get(timeout=30)
saga.status = "compensated"
save_saga_state(saga)
# Retry the whole saga after a backoff; or escalate if fatal.
raise self.retry(exc=exc, countdown=60)
saga.status = "completed"
save_saga_state(saga)
return saga.statusEach step task is idempotent and safe to retry. Here is what reserving inventory might look like:
@app.task
def reserve_inventory_task(order_id):
session = SessionLocal()
try:
# Idempotency check: if already reserved, return success.
order = session.query(Order).filter_by(id=order_id).first()
if order and order.inventory_reserved:
return {"reserved": True, "reservation_id": get_existing_reservation(order_id)}
# Business logic to reserve inventory...
reservation_id = call_inventory_service(order_id)
order.inventory_reserved = True
session.commit()
return {"reserved": True, "reservation_id": reservation_id}
finally:
session.close()And the compensation for it:
@app.task
def release_inventory_task(order_id, reservation_result):
session = SessionLocal()
try:
order = session.query(Order).filter_by(id=order_id).first()
if not order or not order.inventory_reserved:
return {"released": True} # Already compensated or never reserved
call_inventory_service_release(reservation_result["reservation_id"])
order.inventory_reserved = False
session.commit()
return {"released": True}
finally:
session.close()The consumer side, whether it receives the original OrderCreated event from the outbox relay or internal saga commands, must be idempotent. A minimal deduplication guard looks like this:
class ProcessedEvent(Base):
__tablename__ = "processed_events"
idempotency_key = Column(String, primary_key=True)
processed_at = Column(DateTime, default=datetime.datetime.utcnow)
def handle_order_created(session, event_payload, idempotency_key):
if session.query(ProcessedEvent).filter_by(idempotency_key=idempotency_key).first():
return # Already processed
# Do the work.
create_order_from_event(session, event_payload)
session.add(ProcessedEvent(idempotency_key=idempotency_key))
session.commit()Notice the pattern. The outbox guarantees atomic state plus event. The saga guarantees that failures lead to defined compensations. Idempotency guarantees that retries and duplicates do not corrupt state. These three layers stack on top of each other.
One more piece: connecting the outbox to Kafka with Debezium. A simplified Debezium connector configuration for PostgreSQL might look like this:
{
"name": "order-outbox-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "debezium",
"database.password": "dbz",
"database.dbname": "orders",
"topic.prefix": "orderdb",
"table.include.list": "public.outbox",
"tombstones.on.delete": "false",
"transforms": "outbox",
"transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter"
}
}Debezium reads every insert into the outbox table from the PostgreSQL write-ahead log and routes it to a Kafka topic, typically keyed by aggregate_id so that events for the same aggregate land in the same partition and preserve ordering. The application does not publish to Kafka directly; it simply writes to its own database and trusts the CDC pipeline to propagate the event reliably.
This is the shape of a modern Python event-driven backend in 2026: small, focused services, an outbox in the application database, Debezium feeding Kafka, Celery or a durable workflow engine running sagas, and idempotency keys everywhere.
The 2026 Tooling Landscape: Who’s Doing the Heavy Lifting
If you are evaluating technology for this space in 2026, the landscape splits naturally into three layers: the event bus, the durable workflow or saga engine, and the change-data capture glue that connects them.
For the event backbone, Apache Kafka remains the dominant choice, especially through managed offerings like Confluent Cloud. It gives you durable, ordered, partitioned streams and a massive ecosystem. AWS EventBridge is popular in AWS-centric stacks for its serverless event routing and schema registry. NATS JetStream and Apache Pulsar are strong alternatives if you want lighter-weight or geo-replicated messaging.
For change-data capture, Debezium is essentially the default. It supports PostgreSQL, MySQL, SQL Server, MongoDB, and more, and it plugs cleanly into Kafka. Alternatives include database-native logical replication, AWS DMS, and newer database engines that expose change streams directly.
For saga orchestration and durable workflows, Temporal has become a powerhouse. It provides durable execution, retries, timers, signals, and compensation support with SDKs in multiple languages. Orkes Conductor, built on Netflix Conductor, offers a strong cloud-native orchestration option. Camunda remains relevant for BPMN-driven workflows. In the .NET world, MassTransit provides an excellent saga and outbox story that many teams envy. Netflix Conductor continues to influence the orchestration space.
For Python specifically, your choices are more about assembly than single frameworks. Celery and Dramatiq handle task queues. aiokafka and confluent-kafka talk to Kafka. The Temporal Python SDK gives you access to durable execution if your team is comfortable with the model. Orkes Conductor also exposes Python clients. For outbox relaying, you will likely use Debezium or write a small poller.
The trajectory is toward convergence. Platforms are increasingly bundling streaming, CDC, outbox routing, and workflow execution into integrated offerings. That will lower the assembly tax over time, but it will not change the underlying principles.
Resilience Is a Layered Onion: Idempotency, Observability, and Compensation
Patterns are necessary but not sufficient. The research is unambiguous: reliable distributed systems depend on idempotency, deduplication, clearly defined compensating actions, and strong observability. Delivery semantics alone will not save you.
Idempotency is the discipline of making repeated execution safe. Every consumer that can receive the same event twice must detect or tolerate duplicates. The simplest mechanism is an idempotency_key stored in the consumer’s own database, checked at the start of processing within the same transaction that performs the work. This guarantees that even if the process crashes after the work is done but before the ack is sent, a retry will see the stored key and skip the work.
Compensation is not magic. A compensation is a business operation that must be designed, tested, and observed like any other. It must be idempotent, because you may try to run it more than once. It must have clear success and failure semantics. And when a compensation itself fails after retries, your system must have an escalation path, such as moving the saga to a human-reviewed dead-letter queue rather than silently leaving partial state.
Observability is what makes sagas debuggable. You need distributed tracing across the outbox relay, the broker, the saga orchestrator, and the consumers. You need metrics on relay lag, saga step duration, compensation frequency, and consumer lag. You need structured logs that include saga ID, step name, event ID, and idempotency key. A saga that fails in production without observability is a ticket that takes three days to understand. A saga that fails with full tracing is a ticket that takes twenty minutes.
Together these layers form the real resilience strategy. The outbox protects the publisher. The saga defines the recovery path. Idempotency protects against retries. Observability lets you see what went wrong. None of them alone is enough.
The Convergence Curve: Where This All Heads by 2027
If there is one big-picture takeaway from the research, it is this: the boundaries between event streaming, outbox change-data capture, and durable workflow execution are blurring. We are moving from a world where you stitched together Kafka, Debezium, and Temporal by hand to a world where platforms offer more integrated paths.
Expect to see more managed services that combine a transactional database, an event outbox, automatic CDC propagation, and saga orchestration behind cleaner SDKs. Expect multi-language support to improve, including better Python SDKs for durable execution. Expect the operational burden of running an event-driven architecture to decline, even as the conceptual burden stays roughly the same.
Because here is the thing: the patterns themselves are durable. The outbox pattern will still be correct in ten years. Sagas will still be necessary whenever a business process crosses transactional boundaries. Invariant-driven consistency will still be the right way to think about distributed state. The tooling will get prettier, but the ideas will not change.
That is actually good news. Once you understand the layers, you are not at the mercy of any vendor. You can evaluate a new platform by asking the same questions: Does it make state changes and event publication atomic? Does it give me saga execution with compensation? Does it preserve the ordering my invariants require? Does it help me observe and retry safely?
If the answer is yes, it is probably worth your time.
Closing Stanza
So here is the warm sign-off I promised.
May your outbox tables stay small, your Debezium connectors stay healthy, and your compensations never run twice. May your sagas complete more often than they compensate, and when they do compensate, may they do so cleanly and with excellent logs. May your Kafka partitions be evenly keyed, your consumers be idempotent, and your product managers finally believe you when you say eventual consistency is a feature, not a bug.
Building distributed systems is hard. Building them without atomic state-and-event bridges, saga recovery, and clear consistency boundaries is harder. You now have the map.
Keep building the invisible plumbing. Keep choosing tools with intention. And come back tomorrow for the next issue of The Backend Developers—we will still be here, making sense of the chaos, one event at a time.
Take care, and happy shipping.









