System design · Payments · Part 4
Asynchronous work and the outbox pattern
What has to happen before responding and what can wait, why a message broker helps, and how the transactional outbox keeps events from getting lost.
Synchronous and asynchronous
When a customer clicks Pay, some work needs to happen immediately:
- Validate the request
- Check the order
- Create the payment
- Start the provider request
This is synchronous: the client waits for the response.
Other work doesn't need to happen before responding:
- Send the receipt email
- Update analytics
- Write audit events
- Notify the warehouse
- Add loyalty points
- Fraud post-processing
Making the customer wait for all of that would be wasteful. So it runs asynchronously, in the background:
Payment succeeded
- now
Respond to customer
- later
Queue background work
The customer gets an answer right away; the rest happens after.
Why use a message queue
After a payment succeeds, several things should happen:
| Service | Action |
|---|---|
| Order service | Mark the order paid |
| Email service | Send the receipt |
| Analytics | Record the purchase |
| Inventory | Update the stock |
| Rewards | Add points |
1.Calling each service
One simple implementation is for the payment service to call each service itself:
Payment service
Order service
Email service
Down
Analytics service
Slow
Inventory service
Rewards service
One of the five is down. What should the payment service do?
The problems show up when a service misbehaves:
- A service is down. Should the payment fail? No, the payment already succeeded.
- Analytics is slow. Should the customer wait? No.
2.A message broker
This motivates asynchronous messaging. The payment service publishes one event to a message broker, and each service consumes it independently:
Payment service
- PaymentSucceeded event
Message broker
Order service
Email service
Analytics service
Rewards service
The payment service doesn't need to know who's listening, or whether they're up.
Common message brokers include:
Kafka
RabbitMQ
Amazon SQS
Google Pub/Sub
Don't add Kafka by default
Naming a technology isn't a design. Explain the decision, and the problem it solves:
Weak
“I'll use Kafka.”
Names a tool, but not why.
Strong
“Once a payment succeeds, it triggers several independent downstream actions. I'd publish a PaymentSucceeded event through a durable message broker. That decouples the payment request from slower or temporarily unavailable consumers.”
Solves a problem instead of naming a technology.
The transactional outbox pattern
Instead of updating the database and then publishing to Kafka, write both the payment update and an event record in the same database transaction. The payments table gets:
pay_123 → SUCCEEDEDAnd at the same time, outbox_events gets:
id: event_456
type: PaymentSucceeded
payment_id: pay_123
published: falseBoth happen in one transaction:
BEGIN;
UPDATE payments
SET status = 'SUCCEEDED'
WHERE id = 'pay_123';
INSERT INTO outbox_events (...);
COMMIT;Now either both succeed, or both fail. There's no state where the payment succeeded but the event was forgotten.
1.The outbox worker
A background worker reads the rows in outbox_events where published = false, and publishes them to Kafka or SQS.
Payment service
- one DB transaction
PostgreSQL
payment = SUCCEEDED outbox = PaymentSucceeded
- background worker
Message broker
Order service
Email service
Analytics service
The event is saved with the payment, then published by the worker.
If publishing fails, published stays false, and the worker retries later. Much safer.
What does exactly once mean
The goal is one payment, handled exactly once. But distributed systems make true exactly-once processing difficult: Kafka, SQS and webhooks may all deliver an event more than once.
That's fine, as long as consumers handle duplicates safely. PaymentSucceeded might arrive three times:
- PaymentSucceeded
- PaymentSucceeded
- PaymentSucceeded
Order service
Already PAID. Do nothing.
Email service
Event seen. One receipt.
Each consumer makes a repeated event harmless.
- The order service sees the order is already
PAID, and does nothing. - The email service can use the event ID, so it sends only one receipt.