Card purchase
Synchronous authorization, and a single event consumed independently by antifraud, notifications, cashback, and analytics.
This case study shows the most common event-driven architecture pattern in payment systems: a synchronous, critical decision (whether to authorize the purchase) followed by a single event that multiple systems consume completely independently, each concerned with a different responsibility.
The domain event
The authorization itself (approving or denying the purchase at the moment the card is charged) is not
done via Kafka. It's a synchronous call, with a millisecond response, between the payment terminal (or
payment gateway) and the autorizacao-service, which decides based on available balance/limit. This is
intentional: Chapter 1 already explained that request/response communication needing an immediate answer
isn't Kafka's use case.
What happens after the synchronous decision is where Kafka comes in: as soon as the purchase is
approved, the autorizacao-service publishes a CompraAutorizada event (a fact that already happened)
and moves on, without knowing (or needing to know) who will react to it.
{
"eventId": "3a91f7c2-...",
"cartaoId": "card_88213",
"clienteId": "cli_50213",
"valor": 15990,
"estabelecimento": "Loja XYZ",
"categoria": "varejo",
"autorizadoEm": "2026-07-31T18:44:02Z"
}
Topic topology
Topic and configuration
| Item | Decision |
|---|---|
| Topic | compras.autorizadas |
| Key | cartaoId |
| Partitions | 18 |
| Replication factor | 3 |
| Retention | 15 days |
Why the key is cartaoId, not clienteId
Dica de entrevista
The choice between cartaoId and clienteId as the key is a great test of ordering understanding
(Chapter 4). If a customer has multiple cards and the requirement is only "process purchases on the same
card in order" (e.g., to detect a suspicious sequence of purchases on the same card in a short window), the
correct key is cartaoId. If the requirement were to consolidate the customer's behavior as a whole,
regardless of which card was used, clienteId would be the choice, trading per-card ordering for
per-customer ordering. There's no universally correct answer; it depends on which ordering relationship the
domain actually needs.
Independent consumers
- antifraude-service: assesses the purchase against risk rules (spending pattern, geolocation, velocity
between transactions). In suspicious cases, it may publish a separate
CompraSuspeitaevent to trigger a preventive card block. - notificacao-service: sends a push notification, "Purchase of R$ 159.90 approved at Loja XYZ."
- cashback-service: calculates and credits applicable cashback, based on the merchant's category.
- analytics-service: feeds a data warehouse for spending dashboards and recommendation models.
None of these four consumers know the other three exist. A slowdown in analytics-service (for example,
during a heavy report-reprocessing load) doesn't delay the push notification, which needs to be nearly
instant for a good customer experience.
Idempotency: not every consumer needs the same rigor
The level of duplication protection varies by consumer
The four consumers of this event have very different idempotency requirements: a common mistake is applying the same protection pattern (a processed-events table) to all of them, when some are already naturally tolerant of duplication.
Idempotency requirement per consumer
| Consumer | Effect of duplication | Required protection |
|---|---|---|
cashback-service | Cashback credited twice (direct financial loss) | eventId + uniqueness constraint, mandatory |
notificacao-service | Customer gets two notifications (annoying, not critical) | No special protection needed |
antifraude-service | Re-evaluates the same purchase twice (redundant work, not incorrect) | Optional, for efficiency, not correctness |
analytics-service | Purchase counted twice in the aggregation (inflated metrics) | Depends on the query: eventId-based upsert aggregations solve this naturally |
Retry and DLQ
The cashback-service, since it handles direct financial value, uses retry with backoff and a DLQ
(Chapter 9): if the cashback calculation rule fails (e.g., the category table is unavailable), the message
is reprocessed up to 3 times before going to compras.autorizadas.cashback.dlq, without blocking cashback
credit for the following purchases.
Replay
If the cashback category table is retroactively corrected (a category that should give 2% was configured
with 1%), the cashback-service can replay the last 15 days (within retention) to recalculate and credit
the difference, as long as the credit logic is an upsert by eventId, avoiding duplicating cashback
already correctly credited.
Observability
What each team monitors
| Team | Priority metric |
|---|---|
| Cashback | Consumer lag and error rate (a delay here turns into a customer complaint about missing cashback) |
| Notifications | Producer → consumer latency (the push needs to arrive within seconds to have value) |
| Antifraud | DLQ message rate (risk-assessment failures can't go unnoticed) |
| Analytics | Consumer lag aggregated over the day (a delay here is tolerable in minutes, not seconds) |
Resumo
This case illustrates the difference between synchronous communication (the authorization itself, outside
Kafka) and asynchronous (everything reacting to the already-approved purchase). A single event,
CompraAutorizada, is consumed by four systems with completely different latency, idempotency, and
failure-tolerance needs, each configured according to its own criticality, not a single pattern applied
indiscriminately to all of them.
Related chapters