Building reliable distributed systems is hard, and the dual-write problem is among the top challenges. When an application writes to a database and then publishes an event, one of those operations can fail, leaving the system in an inconsistent state. The outbox pattern is a well-known fix: store both the state change and the event intent in the same transaction, then process asynchronously.
In this article, we’ll explore how Dapr implements the outbox pattern. We’ll start with a high-level overview, then go into a Dapr Outbox deep dive, tracing the sequence of operations, the role of the transaction marker and failure handling. The goal is to explain how Dapr addresses the dual-write problem and delivers transactional consistency between state and pub/sub, while also highlighting the tradeoffs involved, so you can make informed configuration choices for your own use case.
Dapr Outbox Pattern Overview
Teams often implement the outbox pattern to solve dual-write requirements, storing both the state change and the event in a single database transaction. Later, a process (such as Debezium) reads from this “outbox” table and delivers the event. While effective, building this by hand ties you to one database, one broker, and leaves you to handle retries, duplicates, and race conditions. Dapr Outbox takes this heavy lifting away.
Enabling outbox in Dapr is as simple as adding a few metadata fields to your state component:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysql-outbox
spec:
type: state.mysql
version: v1
metadata:
- name: connectionString
value: "<CONNECTION STRING>"
- name: outboxPublishPubsub
value: "mypubsub"
- name: outboxPublishTopic
value: "newOrder"
With this configuration in place, every transactional write through Dapr produces a guaranteed event on the configured pub/sub and topic. No custom tables, no polling jobs, just configuration.
Dapr’s transactional state API wraps a database change (create or update operations) and automatically publishes a message to a broker every time state changes. The result: whenever you persist data, Dapr ensures a CloudEvent is sent to the message broker. At a high level, this is how it works:

- State change in the database
Service A, using Dapr’s transactional state API, writes business data (e.g., an order or user update) into one of the dozens of supported databases and finishes its request. - Event delivery to the pub/sub broker
Dapr ensures a CloudEvent is published to the configured pub/sub component. This can be any of the dozens of brokers supported by Dapr (Kafka, Redis, RabbitMQ, Azure Service Bus, etc.). If publishing fails, Dapr retries until the event is successfully delivered. - Event consumption by other services
The pub/sub broker delivers the message to subscribed services. In this example, Service B receives the event, processes it, and stays consistent with the state change in Service A.
The result: whenever you persist data, Dapr ensures an event is sent to the message broker.
The Execution Flow of the Dapr Outbox Pattern
From a processing point of view, the outbox pattern operates through two independent execution paths that run concurrently. One that represents the “user request processing”, and second that is the “background message processing”, the end-to-end processing of a single request can happen in the same Dapr instance, or separate instances part of the same outbox flow.
Step 1: Application Calls the Dapr State API
The request processing flow starts when an app calls the Dapr state API to write state transactionally such as persisting an order, or updating a user profile (depicted by step 1 in the diagram below).
Normally, this is just a write to the state store. With Outbox enabled, the same operation is coupled with an intent to publish a message that we will see next.

Step 2: Dapr Publishes an Intent to the Internal Topic
Before touching the database, Dapr publishes a CloudEvent containing a unique transaction ID (an outbox-uuid) to an internal outbox topic ({namespace}{appID}{topic}outbox). This topic is reserved for Dapr’s internal use, and users do not publish to or consume from it. You can use a dedicated pubsub broker for this purpose, separate from the external topic broker. On some cloud message brokers, you may need to pre-create the topic or ensure Dapr has permissions to create it.
There is also the flexibility to customize the data published to the internal topic through projections and custom CloudEvent headers. Projections allow you to specify a different payload for messaging by adding "outbox.projection": "true" metadata to a state operation with the same key, enabling you to publish filtered or transformed data while storing the complete record in the database. Additionally, you can customize CloudEvent headers by adding metadata to your state operations, which gets automatically propagated to the internal topic messages, allowing for custom routing, filtering, or enrichment of your outbox events.
This internal publish will introduce some latency (a few milliseconds) and an additional failure point if the broker is down. If the publish fails, the whole state API call request is aborted immediately. But it provides an important guarantee: if your transaction commits, there is already a durable record of the intent to deliver an event.
Step 3: State and Marker Are Written Atomically
After the internal publish succeeds, Dapr writes to your state store inside a single transaction:
- Business data — the state you wanted to persist (e.g., user:123 → {"name":"Alice"})
- Transaction marker — a lightweight row such as outbox-uuid-456 → "0"
The marker is stored in the same table as your business data. Its purpose is to act as proof that the transaction was committed. Because both entries are part of the same transaction, they are committed atomically: either both are written, or neither is. This does not introduce a new round trip or transaction; it’s simply an additional row in the same commit.
Step 4: Application Receives the Commit Response
When the transaction commits, Dapr returns success to your application. At that moment, your state is safely persisted, the marker in the database and the intent message in the internal topic together guarantee that an event will eventually be published to the external topic. With this, the user request processing flow is completed.
Step 5,6 & 7: Background Subscriber Verifies Marker and Publishes Event
Independent from user request processing flow, separate background threads handle the internal topic messages. During Dapr startup, the runtime registers message handlers for internal outbox topics. When a message arrives (5), the subscriber extracts the transaction marker ID and looks for the marker in the database (6). If the marker is present in the database, Dapr publishes the verified business event to your configured external topic (7). This ensures events are only emitted after the database commit. If the marker cannot be found, Dapr retries with exponential backoff:
bo := &backoff.ExponentialBackOff{
InitialInterval: time.Millisecond * 500,
MaxInterval: time.Second * 3,
MaxElapsedTime: time.Second * 10,
Multiplier: 3,
Clock: backoff.SystemClock,
RandomizationFactor: 0.1,
}
By default, Dapr retries for up to 10 seconds. If the marker still isn’t found, the message is not acknowledged, and the broker keeps it. Depending on broker configuration, the message could be discarded, or redelivered for Dapr to retry indefinitely.
The option outboxDiscardWhenMissingState=true changes this behavior: instead of retrying indefinitely, Dapr discards the message. This avoids “poison message” loops but trades away guaranteed delivery. So make this change after careful consideration only.
For preserving the order of events, you should use a broker with FIFO ordering guarantees and a single consumer. For scaling, on the other hand, it is recommended to use brokers with competing consumers so messages can be distributed across multiple Dapr instances.
Step 8 & 9: Marker Cleanup and Message Acknowledgment
After successfully publishing to the external topic, Dapr cleans up the marker by deleting (8) it from the state store. If marker deletion fails, the event has already been delivered to an external topic. This does not cause message loss, but the message will be NACKed (9) and retried, which can lead to duplicate external messages. As a result, your consumers must be idempotent.
External consumers can still process the business event independently, confident that the corresponding state change was atomically committed to the database. The background flow remains fully decoupled from the user request, clients receive a success response after the DB transaction commits, while Dapr handles publishing, cleanup, retries, and redelivery in the background.
Atomicity, Consistency, Transactionality
As Martin Kleppmann explains in his Designing Data-Intensive Applications book, atomicity means all-or-nothing within a single database transaction. Achieving atomicity across multiple systems, such as a database and a message broker, requires distributed transactions, something most modern architectures avoid due to their complexity and fragility.
Dapr Outbox takes a different approach. It is atomic within the database, state and marker are written together in one transaction. Across the database and broker boundary, it is a transactional pipeline: the combination of marker verification, retries, and eventual publishing guarantees consistency, though not atomicity. In practice, this means state and events remain aligned, with at-least-once delivery and the requirement for idempotent consumers.
In other words, Dapr Outbox gives you transactional state changes with eventual, reliable event publishing. It is not atomic across systems, but it is scalable, it prevents data drift, and guarantees that events eventually follow state, even in the face of failures.
The outbox pattern is a practical approach for solving the dual-write problem in scalable distributed systems, but implementing it by hand is painful with schema changes, background jobs, retry logic, duplicate handling, and broker-specific quirks. Dapr Outbox bakes this complexity into the API itself. With just a few lines of configuration, every transactional state write becomes a guaranteed event, coordinated with retries and cleanup. It’s atomic within a single database transaction, transactional across the database–broker pipeline, and reliable with clear trade-offs. If you’re building microservices that depend on state and events staying in sync, Dapr Outbox is one of the simplest, most portable (works across dozens of state stores and message brokers) ways to implement the outbox pattern at scale.
Find out more...
To build your practical knowledge:
- Start with “How-To: Enable the transactional outbox pattern” for core configuration.
- Read the “Publish & subscribe overview” for context around transactional messaging.
- Dive into “Workflow patterns” to see how workflows align with messaging and state.
- Bring it all together with Dapr University – try out quickstarts and interactive labs around state, pub/sub, and workflows.