The full picture
- Why not 2PC: synchronous coupling of availability (all participants + coordinator up), in-doubt transactions holding locks during coordinator recovery, throughput collapse under latency variance, poor/no support in Kafka, many NoSQL stores and serverless DBs — and it still doesn't cover 'call a third-party REST API'.
- Outbox mechanics: INSERT business rows + INSERT event row in one local TX → Debezium/CDC (or a poller) publishes → consumer processes idempotently. Guarantee: at-least-once, ordered per aggregate — pair with consumer dedup (event id table or natural idempotency).
- Saga trade-offs: orchestration centralizes visibility/retry/timeout logic (Temporal, Camunda, a state-machine table); choreography decentralizes but the flow becomes emergent — hard to answer 'where is order 123 stuck?'. Compensations are business actions (refund, release stock), not DB rollbacks — some things can't be uncompensated (sent email), so sequence risky steps last.
- Semantic anchors: isolation is gone between steps — other transactions see intermediate states; design 'pending' states into the domain model explicitly.
@Transactional
public void placeOrder(Cart cart) {
var order = orderRepo.save(Order.from(cart)); // same local TX
outboxRepo.save(OutboxEvent.of("OrderPlaced", // same local TX
order.id(), toJson(order))); // -> atomic ✅
} // CDC tails the outbox table and publishes to Kafka.
// Crash anywhere: either both rows exist or neither. No dual-write gap.