trendingzones
← Back to the Intermediate level

JAVA + SPRING AI — ADVANCED

Database-per-Service, Eventual Consistency & the Saga Pattern

The Intermediate level’s demo assumed every service could just answer a request correctly and instantly. Real systems have to handle what happens when a multi-service operation partially completes — and that starts with a decision about where each service’s data actually lives.

The Quick Answer

Each microservice owns its own database, with no other service allowed to query it directly — the same independence covered at the Introduction level extends all the way down to data. That independence has a real cost: you lose the ability to run one SQL join across two services’ tables, or wrap a change to both services’ data in one shared database transaction. The accepted trade-off is eventual consistency— for a brief window, different services’ data can be temporarily out of sync, with the system designed to converge back to a consistent state shortly after. The Saga pattern is the standard way to coordinate a multi-step operation across services under that constraint, and Spring Cloud Stream, backed by a message broker like Kafka or RabbitMQ, is how Spring Boot services typically publish and react to the events a saga depends on.

Why Database-per-Service

It would be technically simple for Order Service, Inventory Service, and Payment Service to all point at one shared database. It would also quietly undo most of what the Introduction level covered. If every service can reach into every table, nothing stops Order Service from depending on some internal detail of Inventory Service’s schema — and the moment that happens, Inventory Service can no longer change its own schema, deploy on its own schedule, or even switch database technologies, without risking breaking Order Service too. The independence a microservices architecture is built for — independent deployment, independent scaling, one team owning one service’s decisions — depends on each service’s data being just as off-limits to other services as its internal Java classes already are.

So the rule is: every service gets its own database (or its own schema, at minimum, depending on how strict the isolation needs to be), and every other service that needs that data has to ask for it — through a REST call like the ones built in the Intermediate level, or through an event, covered below — never through a direct database connection.

The Consistency Problem This Creates

Database-per-service isn’t free. Two capabilities a single shared database gives you for granted disappear:

The accepted answer to this isn’t to somehow force cross-database atomicity back into existence — it’s eventual consistency: accept that data can briefly disagree across services, and build a deliberate mechanism to bring it back into agreement, rather than pretending the problem doesn’t exist.

The Saga Pattern

A sagais a sequence of local transactions, one per service — each one updates that service’s own database and then triggers the next step. If a step later in the sequence fails, the saga runs compensating transactionsthat undo the effects of whichever earlier steps already completed — the closest equivalent to a rollback that’s available once one shared database transaction is no longer possible. This is a well-established distributed-systems pattern, not something specific to Spring; microservices.io — a widely used canonical reference for microservices patterns — describes exactly this shape, and the two coordination styles below, the same way.

Choreography: No Central Coordinator

In a choreographed saga, there is no single component directing the whole sequence. Each service does its own local work, then publishes an event announcing what happened; other services listen for events that matter to them and react on their own. For placing an order: Order Service creates a pending order and publishes an OrderCreated event. Inventory Service, listening for that event, tries to reserve stock and publishes either StockReserved or StockReservationFailed. Order Service, listening for those events in turn, either moves the order forward or triggers a compensating action (canceling the order) — all without any service needing to know the full sequence end to end.

Orchestration: A Central Saga Coordinator

In an orchestratedsaga, a dedicated coordinator component explicitly directs the whole sequence — it sends a command to a participant (“reserve this stock”), waits for that participant’s reply, and decides what to do next based on that reply, including issuing compensating commands to earlier participants if a later step fails. The saga orchestrator holds the entire sequence’s logic in one place, rather than that logic being implicitly spread across every service’s event listeners.

Neither style is universally “better” — choreography avoids a central point of coordination logic but can make the overall flow harder to see in any one place; orchestration makes the flow explicit and easy to follow but introduces a coordinator that every participant now depends on. Teams pick based on how complex the saga’s logic actually is and how much visibility they want into the sequence as a whole.

Event-Driven Coordination: Spring Cloud Stream

Both saga styles above depend on services being able to publish and react to events without directly querying each other’s databases. Spring Cloud Streamis Spring’s current, actively maintained abstraction for exactly this — publishing and consuming messages through a broker like Kafka or RabbitMQ, without your application code needing to know the specific broker’s client API. Here’s Order Service publishing that OrderCreated event from the choreography example above, using StreamBridge — the current, idiomatic way to publish a message on demand (as opposed to on a fixed schedule):

public record OrderCreated(Long orderId, String sku, int quantity) {}

@Service
public class OrderService {

    private final StreamBridge streamBridge;

    public OrderService(StreamBridge streamBridge) {
        this.streamBridge = streamBridge;
    }

    public void placeOrder(String sku, int quantity) {
        Long orderId = /* save the pending order to Order Service's own database */ 42L;

        // Publishes onto the "orders-out-0" binding, which points at a
        // specific topic/queue on whichever broker (Kafka, RabbitMQ, ...)
        // is configured — application code never touches the broker's
        // own client API directly.
        streamBridge.send("orders-out-0", new OrderCreated(orderId, sku, quantity));
    }
}

Inventory Service, running as its own separate Spring Boot application with its own separate database, would listen for messages on the corresponding input binding, attempt to reserve stock against its own data, and publish its own outcome event in response — exactly the choreography sequence described above, with Spring Cloud Stream as the plumbing that gets each event from one service’s publisher to another service’s listener.

Where This Goes Next

That closes out Phase 1 of this pillar. Across three topics, you’ve gone from a single Spring bean, to a full REST API, to several independent Spring Boot services coordinating over the network with their own separate data. None of that becomes obsolete once Phase 2 begins — beans, REST controllers, and now microservices are the exact foundation the next topic, ChatClient + Ollama Integration, builds Spring AI’s chat capability on top of.

Fun Fact

The Saga pattern’s name comes from a 1987 database research paper, decades before “microservices” was a common term at all — it originally described breaking a single long-running database transaction into a series of smaller ones. Microservices architectures didn’t invent the idea; they simply became the context where that older idea turned out to be exactly the tool needed.

Test Yourself

Why does each microservice own its own database rather than sharing one central database?

What real problem does database-per-service create?

In a choreography-based saga, how do services coordinate?

In an orchestration-based saga, what does the orchestrator do?

What is Spring Cloud Stream’s role in event-driven coordination between microservices?