Outbox Pattern¶
Trigger: Cosmos DB Change Feed + HTTP | State: stateful | Guarantee: at-least-once | Difficulty: advanced
Overview¶
The examples/reliability/outbox_pattern/ sample demonstrates the transactional outbox pattern with
Azure Functions, Azure Cosmos DB, and a change feed relay. The HTTP function writes the business
document and an outbox event in the same Cosmos DB transactional batch, and a second function reads
the outbox event from the change feed and relays it to a broker boundary.
This pattern prevents the classic failure window where business data is committed but event publication fails afterward. By storing the outbox record in the same partition and transaction as the business write, the system guarantees that either both records are committed or neither is.
When to Use¶
- You need to persist business state and publish an integration event without losing one or the other.
- You already use Azure Cosmos DB and can keep the business document and outbox event in the same logical partition.
- You can tolerate at-least-once relay behavior and design consumers to be idempotent.
When NOT to Use¶
- You need exactly-once delivery semantics across the entire pipeline.
- Your business record and outbox event cannot share the same Cosmos DB partition boundary.
- A direct synchronous HTTP call to another service is the real requirement, not durable event relay.
Architecture¶
flowchart LR
client[Client] --> write[HTTP write function]
write --> batch[Cosmos DB transactional batch]
batch --> order[Business document]
batch --> outbox[Outbox event document]
outbox --> relay[Cosmos DB change feed relay]
relay --> broker[Message broker]
Behavior¶
sequenceDiagram
participant Client
participant API as HTTP write function
participant Cosmos as Cosmos DB container
participant Feed as Change feed relay
participant Broker as Message broker
Client->>API: POST /api/outbox/orders
API->>Cosmos: transactional batch(order, outbox event)
alt batch succeeds
Cosmos-->>API: commit both documents
API-->>Client: 202 Accepted
Cosmos-->>Feed: deliver changed outbox document
Feed->>Broker: publish event
Feed-->>Feed: log dispatch / write audit row
else batch fails
Cosmos-->>API: rollback
API-->>Client: 500 error
end
Prerequisites¶
- Python 3.10+
- Azure Functions Core Tools v4
- Azure Cosmos DB account or emulator with database
outboxdband containersordersandleases - SQLite CLI, or another database supported by
azure-functions-db-python, for relay audit storage
Project Structure¶
examples/reliability/outbox_pattern/
|-- function_app.py
|-- host.json
|-- local.settings.json.example
|-- pyproject.toml
`-- README.md
Implementation¶
The write endpoint accepts an order payload, builds two documents, and submits them in one Cosmos DB transactional batch:
- an
orderdocument that represents the business state - an
outboxdocument that represents the integration event to relay later
Both documents use the same partition key so Cosmos DB can commit them atomically.
operations = [
("create", (order_document,)),
("create", (outbox_document,)),
]
container.execute_item_batch(batch_operations=operations, partition_key=partition_key)
The relay function listens to the Cosmos DB change feed, filters for type == "outbox", publishes
the event to a broker boundary, and records dispatch metadata with azure-functions-db-python. The sample
uses structured logging to make the relay path visible during local runs.
@app.cosmos_db_trigger(...)
@db.output("dispatch_out", url="%DISPATCH_DB_URL%", table="outbox_dispatches")
def relay_outbox_events(documents: list[dict[str, Any]], dispatch_out: DbOut) -> None:
dispatches = []
for document in documents:
if document.get("type") != "outbox":
continue
publish_to_broker(document)
dispatches.append(build_dispatch_record(document))
dispatch_out.set(dispatches)
Run Locally¶
cd examples/reliability/outbox_pattern
pip install -e ".[dev]"
cp local.settings.json.example local.settings.json
sqlite3 relay.db "CREATE TABLE IF NOT EXISTS outbox_dispatches (event_id TEXT PRIMARY KEY, aggregate_id TEXT NOT NULL, event_type TEXT NOT NULL, broker_name TEXT NOT NULL, dispatched_at TEXT NOT NULL, status TEXT NOT NULL);"
func start
Expected Output¶
[INFO] Accepted order and stored outbox event in one transactional batch. order_id=order-1001 event_id=evt-order-1001-created
[INFO] Received 2 changed document(s) from Cosmos DB.
[INFO] Relayed outbox event to broker boundary. event_id=evt-order-1001-created broker=log-broker
[INFO] Recorded 1 dispatch audit row(s).
Production Considerations¶
- Partitioning: keep the business document and outbox event in the same logical partition or the batch is not atomic.
- Ordering: preserve stable event ordering per aggregate, especially when one order emits multiple events.
- Deduplication: assume the relay can replay and make downstream consumers idempotent.
- Cleanup: apply TTL to outbox records or archive them after the replay window expires.
- Broker integration: replace the sample's logging publisher with Service Bus, Event Hubs, or Kafka.
- Observability: log
order_id,event_id, and relay outcome so failures can be replayed safely.