Keeping data consistent across microservices (database per service vs shared database, sagas, outbox, eventual consistency vs 2PC), how a saga works with compensations (a concrete booking/order example), consumer groups for fan-out to multiple services, building real-time notifications on Kafka with Spring Boot, and choosing sync vs async communication between User/Order/Payment services.
Published September 25, 2026
Consistency is the senior microservices topic. Show that you:
For Kafka, get consumer groups exactly right. It's a very common place to go wrong.
Short answer: Accept that there's no cross-service ACID transaction, and design for it:
PENDING → CONFIRMED/CANCELLED), so intermediate states are explicit and visible.Learn it in depth → Eventual Consistency Design
Short answer: Database per service (at least a schema per service with no cross-access) for real microservices:
The costs:
When a shared database is acceptable: as a transitional step while migrating from a monolith, for small systems owned by one team, or for read-only shared reference data. Even then, give each service its own schema and credentials, so coupling is visible and controlled.
Learn it in depth → Data Ownership Model
Short answer: A saga breaks one business transaction into a series of local transactions, one per service. Each step commits locally, and triggers the next through an event or command. If a step fails, the saga runs compensating transactions for the completed steps, in reverse order. The result is eventual consistency, without distributed locks.
It comes in two styles:
OrderCreated → Payment charges → PaymentCompleted → Inventory reserves. There's no central coordinator; it's simple for short flows, but hard to follow as it grows.Key points to cover:
PENDING status);Learn it in depth → Saga Pattern
Short answer: A travel booking saga (flight → hotel → car):
FLIGHT_HELD).HOTEL_CONFIRMED).FAILED, and the customer is notified. If money was already taken, the compensation is a refund.An e-commerce order saga:
| Step | Action | Compensation |
|---|---|---|
| 1 | Order service: create order PENDING | Mark order CANCELLED |
| 2 | Inventory: reserve stock | Release reservation |
| 3 | Payment: authorise card | Void authorisation / refund |
| 4 | Shipping: create shipment | (fails → trigger 3, 2, 1 compensations) |
Key points to cover:
// Orchestrator step handling (simplified)
void onShipmentFailed(ShipmentFailed e) {
Saga saga = sagas.load(e.sagaId());
saga.markCompensating();
commands.send(new VoidPayment(saga.orderId(), saga.paymentAuthId())); // each command is idempotent (sagaId as key)
commands.send(new ReleaseStock(saga.orderId()));
commands.send(new CancelOrder(saga.orderId(), "SHIPPING_UNAVAILABLE"));
sagas.save(saga);
}
Short answer: Publish once to one topic. Give each service its own consumer group (group.id=notification-service, group.id=analytics-service, group.id=fraud-service).
Common trap: the source puts all the services "as part of a consumer group". If they shared one group, each message would go to only one of them. Different services need different group IDs.
Key points to cover:
orderId), so each service sees that entity's events in order.Short answer:
PostLiked, CommentAdded, UserFollowed and so on to topics keyed by the recipient's user ID, to preserve per-user ordering. Use the transactional outbox for reliability.@KafkaListener, its own consumer group, concurrency ≈ its partition count):
acks=all, idempotence.DefaultErrorHandler + DLT.spring:
kafka:
bootstrap-servers: ${KAFKA_BROKERS}
producer:
acks: all
properties:
enable.idempotence: true
consumer:
group-id: notification-service
auto-offset-reset: earliest
properties:
spring.json.trusted.packages: "com.social.events"
listener:
ack-mode: record
Learn it in depth → Design a Notification Service
Short answer: Choose per interaction:
Synchronous (REST or gRPC) when the caller needs the answer to continue, and the user is waiting. For example:
UserUpdated events, to avoid the runtime dependency.Wrap these calls in timeouts, a circuit breaker and retries (idempotent calls only, or with an idempotency key).
Asynchronous (Kafka or RabbitMQ) for side effects and long workflows, where decoupling and resilience matter more than immediacy:
OrderPlaced → Inventory, Notifications, Analytics;PaymentCompleted → Order confirms, Shipping starts.It's also how the saga's steps are chained.
Key points to cover:
Short answer:
First, remove the "shared data". Give it one owner service. Others change it only through that owner's API or commands, and read it through replicated read models.
Eventual consistency (the default):
It scales, and tolerates failures. The price is temporary inconsistency, which the business must accept (and usually already does: "payment processing").
Distributed transactions (2PC/XA):
Where strong consistency is essential (an account balance), keep that invariant inside one service and one database transaction. Design the boundaries so it doesn't span services.
Learn it in depth → Outbox Pattern
Q: How does the transactional outbox work?
A: In the same local transaction as the business change, insert the event into an outbox table. A relay then publishes the outbox rows to Kafka, and marks them sent. The relay is either a poller, or CDC with Debezium reading the write-ahead log. Delivery is at-least-once, so consumers deduplicate by event ID.
Q: Does Kafka's "exactly-once" remove the need for idempotent consumers?
A: Only within Kafka (read → process → write to Kafka, with transactions and read_committed). Side effects in external systems (databases, emails, APIs) still need idempotency, or the outbox/inbox pattern.
Q: Choreography or orchestration: how do you choose? A: Choreography for short, simple flows with few participants, where maximum decoupling matters. Orchestration for long, complex flows that need timeouts, visibility, error handling and business-level monitoring. Many systems use orchestration for core flows, and events for peripheral reactions.
Q: How do you query data spread across services (for example, "orders with customer names")? A: With API composition (a BFF or gateway calls both services, and joins in memory) for simple cases, or CQRS read models: a query service subscribes to Order and User events, and maintains a denormalised view optimised for that query.