Data Management Per Service
Data Management Per Service
One of the most consequential decisions in a microservices architecture is also one of the most misunderstood: each service must own its data exclusively. This principle — sometimes called Database per Service — means a service's tables, schema, and datastore are entirely private. No other service reads them directly; all data access goes through the service's own API. This lesson explains why that rule exists, how to implement it with Spring Boot 3, and what trade-offs you accept when you follow it.
Why a Shared Database Is an Anti-Pattern
Sharing a single database across multiple services seems tempting: you avoid duplication, joins are trivial, and you already know the schema. In practice it creates tight coupling at the worst possible layer:
- Schema coupling: A column rename in the
orderstable breaks every service that queries it, even ones maintained by a different team. - Deployment coupling: You cannot roll out a new version of the
inventoryservice if theorderservice still expects the old schema. - Scaling coupling: You cannot run the read-heavy
catalogservice on a read replica while the write-heavycheckoutservice uses the primary; they share the same connection pool. - Technology coupling: One team wants PostgreSQL with JSONB; another wants a document store. A shared database forces a single engine on everyone.
Polyglot Persistence
Database-per-service frees each team to choose the right storage technology for their problem:
- Order service — relational (PostgreSQL) for ACID transactions.
- Catalog service — document store (MongoDB) for flexible product attributes.
- Cart service — key-value store (Redis) for ephemeral, low-latency session data.
- Search service — search engine (Elasticsearch) for full-text queries.
Spring Boot's auto-configuration supports all of these. You declare a dependency and configure a connection URL; Spring wires the rest.
Configuring Isolated Datastores in Spring Boot 3
Each service has its own application.yml with its own datasource. The order-service might look like this:
The catalog-service uses a completely separate MongoDB instance:
ddl-auto: create or create-drop in production. Always use a migration tool — Flyway or Liquibase — so schema changes are versioned, auditable, and reversible. Flyway is the default auto-configured option in Spring Boot.
Schema Migration with Flyway
Each service carries its own migration scripts under src/main/resources/db/migration. The naming convention is V{version}__{description}.sql:
Flyway runs automatically on application startup (before the HTTP port opens), applies any pending migrations in version order, and records each applied script in a flyway_schema_history table. If a migration fails, the application refuses to start — which is exactly what you want: a half-migrated schema is worse than no schema.
The Cost: No Cross-Service Joins
Once each service owns its data, a query like "get the order together with the customer's name and the product's current price" can no longer be a SQL join. You have three options:
- API Composition — the caller (or a dedicated aggregation service) fetches from multiple services and stitches the result in memory. Simple but adds latency and creates a caller-side dependency on multiple APIs.
- Event-Driven Denormalization — services publish events when data changes; consumers maintain their own local read-optimized copies. Eventual consistency, but read performance is excellent.
- CQRS + Read Models — a dedicated read service subscribes to domain events and materializes a joined view in its own store (e.g., a denormalized Elasticsearch index).
The most common starting point is API composition for simple queries plus event-driven updates for data that changes frequently. Here is a typical API composition in the order-service:
OrderService imports CustomerRepository from the customer-service module, you have recreated the shared-database problem in code — now both services must be deployed together. Cross-service data access always goes through the network API.
Handling Distributed Consistency
Without a shared database, a multi-service operation — such as "create an order and deduct inventory and charge the customer" — cannot be wrapped in a single ACID transaction. You must choose a consistency strategy:
- Saga pattern (choreography): Each service publishes a domain event after its local transaction commits. Downstream services react to that event and publish their own. Compensating transactions roll back on failure.
- Saga pattern (orchestration): A dedicated saga orchestrator issues commands to each service and tracks state. Easier to reason about but adds a coordinator component.
- Eventual consistency + idempotency: Accept that different services will be briefly out of sync. Design every operation to be idempotent (safe to replay) so retries after network failures do not duplicate effects.
The key insight is that even traditional databases are eventually consistent across replicas. Microservices just make that trade-off explicit.
Security Implications
Private databases improve the security posture of the overall system:
- A compromised
catalog-servicecannot readordersorpaymentstables — they are on separate servers with separate credentials. - Each service runs with a database user that has only the permissions it needs (least-privilege principle). The
catalog-serviceuser cannotDROP TABLEin the orders schema even if it somehow learned the connection string. - Sensitive data (PII, payment info) can be isolated in a service whose database is in a higher-security network segment, encrypted at rest with its own key, and audited independently.
application.yml or commit them to git. Spring Cloud Vault and Spring Cloud Config both support this pattern out of the box.
Summary
Database-per-service is the cornerstone of true service autonomy. Each Spring Boot service declares its own datasource in its own application.yml, manages its schema with Flyway migrations that live alongside the service code, and exposes its data only through its REST or messaging API. The cost is that cross-service queries become API composition or event-driven read models, and multi-service transactions require sagas rather than ACID commits. These trade-offs are real, but they are the price of independent deployability, technology freedom, and isolated failure domains — the core promises of the microservices style.