Full Flow
Scenario#
User places an order. The system must: 1. Save the order 2. Update the read model 3. When order ships — update status + send shipping confirmation email
Step 1: User Places Order#
App Service receives the HTTP request.
-- App Service: one atomic transaction
BEGIN TRANSACTION
INSERT INTO order_events (order_id, event_type)
VALUES (123, 'OrderCreated')
INSERT INTO outbox (event_type, payload)
VALUES ('OrderCreated', '{"order_id": 123, "amount": 49.99}')
COMMIT
Order is saved in event store. Outbox row written atomically.
Step 2: Debezium Picks Up Outbox Row#
Debezium tails the WAL, sees the new outbox INSERT, publishes to Kafka:
Kafka topic: order-events
Message: { event_id: "evt-001", event_type: "OrderCreated", order_id: 123 }
Near real-time — milliseconds after commit.
Step 3: Read Model Updater Consumes OrderCreated#
BEGIN TRANSACTION
-- Inbox: dedup check
INSERT INTO inbox (event_id) VALUES ('evt-001')
ON CONFLICT (event_id) DO NOTHING
-- Update read model
INSERT INTO order_read_model (order_id, status, amount)
VALUES (123, 'created', 49.99)
ON CONFLICT (order_id) DO UPDATE SET status = 'created'
COMMIT
Read model now shows order 123 as 'created'. User can see their order immediately.
Step 4: Order Gets Shipped#
Warehouse system triggers OrderShipped. App Service appends to event store + outbox:
BEGIN TRANSACTION
INSERT INTO order_events (order_id, event_type)
VALUES (123, 'OrderShipped')
INSERT INTO outbox (event_type, payload)
VALUES ('OrderShipped', '{"order_id": 123, "tracking": "UPS123"}')
COMMIT
Step 5: Debezium Publishes OrderShipped#
Kafka topic: order-events
Message: { event_id: "evt-002", event_type: "OrderShipped", order_id: 123 }
Two consumers are subscribed: Read Model Updater and Order Service.
Step 6: Read Model Updater Consumes OrderShipped#
BEGIN TRANSACTION
INSERT INTO inbox (event_id) VALUES ('evt-002')
ON CONFLICT (event_id) DO NOTHING
UPDATE order_read_model
SET status = 'shipped', tracking = 'UPS123'
WHERE order_id = 123
COMMIT
Read model updated. User sees "shipped" status.
Step 7: Order Service Consumes OrderShipped#
Needs to update DB status AND trigger email. Uses inbox + outbox:
BEGIN TRANSACTION
-- Inbox: dedup check
INSERT INTO inbox (event_id) VALUES ('evt-002')
ON CONFLICT (event_id) DO NOTHING
-- Update order status
UPDATE orders SET status = 'shipped' WHERE order_id = 123
-- Outbox: queue email event
INSERT INTO outbox (event_type, payload)
VALUES ('SendShippingEmail', '{"order_id": 123, "email": "user@gmail.com", "tracking": "UPS123"}')
COMMIT
Step 8: Debezium Publishes SendShippingEmail#
Kafka topic: email-events
Message: { event_id: "evt-003", event_type: "SendShippingEmail", order_id: 123 }
Step 9: Email Service Consumes SendShippingEmail#
Terminal action — email is an external call. Act first, mark after:
# 1. Send the email first (external call)
email_client.send(
to="user@gmail.com",
subject="Your order has shipped!",
body="Tracking: UPS123"
)
# 2. Mark as processed after
db.execute("""
INSERT INTO inbox (event_id) VALUES ('evt-003')
ON CONFLICT (event_id) DO NOTHING
""")
If crash after email but before marking → duplicate email on retry. Acceptable. If crash before email → retry sends email. Correct.
Full Flow Diagram#
sequenceDiagram
participant U as User
participant AS as App Service
participant DB as Postgres
participant DZ as Debezium
participant K as Kafka
participant RM as Read Model Updater
participant OS as Order Service Consumer
participant ES as Email Service
U->>AS: Place Order
AS->>DB: BEGIN TXN: order_events + outbox(OrderCreated) COMMIT
DB->>DZ: WAL stream
DZ->>K: publish OrderCreated (evt-001)
K->>RM: OrderCreated
RM->>DB: BEGIN TXN: inbox(evt-001) + update read_model COMMIT
Note over DB: Order ships later
AS->>DB: BEGIN TXN: order_events + outbox(OrderShipped) COMMIT
DB->>DZ: WAL stream
DZ->>K: publish OrderShipped (evt-002)
K->>RM: OrderShipped
RM->>DB: BEGIN TXN: inbox(evt-002) + update read_model COMMIT
K->>OS: OrderShipped
OS->>DB: BEGIN TXN: inbox(evt-002) + update orders + outbox(SendShippingEmail) COMMIT
DB->>DZ: WAL stream
DZ->>K: publish SendShippingEmail (evt-003)
K->>ES: SendShippingEmail
ES->>ES: send email (external call)
ES->>DB: INSERT INTO inbox(evt-003) ON CONFLICT DO NOTHING Summary of Guarantees#
| Step | Guarantee | Mechanism |
|---|---|---|
| Order saved + event queued | Atomic | Single DB transaction |
| Event published to Kafka | At-least-once | Outbox + Debezium |
| Read model updated exactly once | Exactly-once DB write | Inbox + transaction |
| Order status updated exactly once | Exactly-once DB write | Inbox + transaction |
| Email sent | At-least-once | Terminal action: act first |
Key Insight#
No single step in this flow is magic. Each step uses the same two tools — transactions for atomicity, idempotency for duplicate safety. The entire reliability of the system comes from consistently applying these two primitives at every boundary.