Phase 3: Architecture Patterns

Database-per-service, saga pattern & eventual consistency

Intermediate ~3 min read
Think of it this way A friendly analogy. Read this if the technical version feels dense. Show Hide

Imagine you're building a giant, amazing Lego castle, much bigger than any set you’ve ever seen! Instead of having one huge box of all the Lego bricks for the entire castle, which would be a total mess, you decide to give each main room or section its own special box of bricks. So, the kitchen gets its specific bricks (oven, sink), the tall tower gets its own box (arches, flags), and the royal bedroom gets another. This is super helpful because the builders for the kitchen can work on their part, choose exactly the right bricks for their kitchen, and even change things around without touching or messing up the tower's bricks or bothering the tower builders. Each part is independent and knows its own stuff best.

Now, what if you want to build something really big that needs parts from the kitchen, the tower, and the bedroom, like a new "grand banquet hall"? You can't just grab all the bricks at once because they're in different boxes. Instead, you start a sequence of steps. The kitchen builder adds a fancy oven and sends a note to the tower builder: "Oven done, your turn for the grand entrance!" The tower builder adds an archway and sends a note to the bedroom: "Archway done, now add the royal banner!" Each part does its own small job and then tells the next part to go.

But what happens if the bedroom builder opens their box and realizes they don't have the right royal banner bricks? Oh no! They can't finish their part. Instead of the whole project being stuck, they send notes back to the tower and kitchen builders. "Couldn't do banner, tower, please remove the archway!" And then, "Kitchen, please take out the oven!" This way, if one part fails, you can undo all the previous steps, putting everything back to how it was before you started. It might take a moment for all the notes to travel and for things to be put back. But eventually, everything settles down to a clear, consistent state again, even if it’s "back to before we started." This "eventual settling" is how these big, independent systems stay organized.

So, when you build huge online systems, like for a giant toy store or a video game with millions of players, you can use these ideas. Instead of one giant, impossible-to-manage program, you break it into many smaller, focused "rooms" or "sections," each with its own storage and responsibilities. This means you can change one small part, like the payment system, without shutting down the whole game or store. And even when many parts need to work together for something big, like processing an order, they can do it step-by-step, sending messages, and safely undoing things if there's a problem. It makes building super-large, flexible, and robust software much easier, just like building a sprawling Lego castle piece by piece!

In microservices architecture, Database-per-service is a fundamental principle where each individual microservice owns and manages its own dedicated data store. This design choice grants services complete autonomy, allowing them to choose the best database technology for their specific needs, scale independently, and develop without impacting other services' data. However, this isolation means you can no longer rely on traditional ACID transactions (Atomicity, Consistency, Isolation, Durability) that span multiple services, as there's no single transaction coordinator across disparate databases.

To handle business processes that inherently require updates across multiple services, the Saga pattern emerges as a solution for distributed transactions. A saga is a sequence of local transactions, where each local transaction updates its own service's database and publishes an event. Subsequent services then react to these events, performing their own local transactions. Crucially, if any step in the saga fails, compensation transactions are executed in reverse order to undo the changes made by preceding successful steps, ensuring the system can revert to a consistent state. Sagas can be implemented via choreography (services communicating directly via events) or orchestration (a central service coordinating the saga).

This approach naturally leads to eventual consistency. During the execution of a saga, the overall system state might be temporarily inconsistent. For example, an order might be marked as "created" by the Order Service, but the Inventory Service might not have completed reserving items yet. The system doesn't guarantee immediate consistency across all services; instead, it guarantees that data will eventually converge to a consistent state once all saga steps (or compensation steps) have successfully completed. This trade-off between immediate consistency and increased availability, scalability, and resilience is a cornerstone of robust microservices design.

Key Takeaways

  • Database-per-service provides service autonomy but prevents traditional ACID transactions across multiple services.
  • The Saga pattern manages distributed transactions as a series of local transactions, using events for inter-service communication.
  • Compensation transactions are vital components of a saga, designed to undo previous changes if any part of the distributed process fails.
  • Eventual consistency is the inherent outcome: data across services might temporarily diverge during a saga but will eventually become consistent, prioritizing availability and scalability.

Code Example

python
# Pseudo-code illustrating a Choreography Saga

# Service: Order
def create_order(details):
    db.save(Order(details, status='PENDING')) # Local transaction 1
    event_bus.publish('OrderCreated', {'orderId': details.id, 'items': details.items})

# Service: Inventory
def handle_OrderCreated(event):
    if inventory.reserve(event.items): # Local transaction 2
        db.update_inventory_status(event.items, 'RESERVED')
        event_bus.publish('InventoryReserved', {'orderId': event.orderId})
    else:
        event_bus.publish('InventoryFailed', {'orderId': event.orderId}) # Triggers compensation

# Service: Payment
def handle_InventoryReserved(event):
    if payment_gateway.process(event.orderId, event.amount): # Local transaction 3
        db.record_payment(event.orderId, 'PAID')
        event_bus.publish('PaymentProcessed', {'orderId': event.orderId})
    else:
        event_bus.publish('PaymentFailed', {'orderId': event.orderId})
        # Order and Inventory services would listen to 'PaymentFailed' to compensate/revert

How this code works

This code illustrates a Choreography Saga, a pattern essential for coordinating business processes across independent microservices, each managing its own data store (Database-per-service). It shows how services achieve eventual consistency by reacting to events, rather than using a single, long transaction.

The flow begins with create_order in the Order service, which saves the order as PENDING and publishes an OrderCreated event via the event_bus. The Inventory service's handle_OrderCreated then attempts to reserve items; if successful, it publishes InventoryReserved. Next, the Payment service's handle_InventoryReserved processes the payment, publishing PaymentProcessed on success. A subtle but crucial aspect is the compensation mechanism: if inventory.reserve fails, an InventoryFailed event is published to trigger cleanup. Likewise, PaymentFailed requires the Order and Inventory services to listen and revert their respective actions, ensuring all data is consistently rolled back if any step in the overall saga fails.