Database per Service Pattern in Java – Data Isolation for Scalable Microservices

Illustration for Database per Service Pattern in Java – Data Isolation for Scalable Microservices
By Last updated:

Introduction

In monolithic applications, a single shared database often serves the entire application. But in a microservices architecture, this approach becomes a bottleneck. Enter the Database per Service Pattern—a foundational principle that assigns each service its own private database, ensuring data ownership, loose coupling, and scalability.

This tutorial explains the Database per Service pattern in depth using Java-based microservices. You'll learn why this pattern matters, how to implement it, and how to handle challenges like cross-service queries.


🧠 What Is the Database per Service Pattern?

The Database per Service pattern assigns each microservice its own dedicated data store. This enables services to operate independently, even at the data level.

UML-style Diagram

[Order Service] ---> [Order DB]
[Customer Service] ---> [Customer DB]
[Inventory Service] ---> [Inventory DB]

🎯 Intent and Participants

Intent:
To promote loose coupling and high autonomy by isolating the database schema of each microservice.

Participants:

  • Service: Business logic holder with exclusive access to its DB.
  • Database: Tied to the service; not accessible by other services.
  • API Layer: Fetches related data across services when needed.

🌍 Real-World Use Cases

  • E-commerce platforms where orders, products, and users are distinct services.
  • Banking systems where accounts and transactions are separate microservices.
  • Logistics apps with services like shipment, tracking, and billing.

🧰 Implementation Strategies in Java

1. Spring Boot Services with Dedicated PostgreSQL Databases

  • customer-service uses customer_db
  • order-service uses order_db

application.yml for customer-service

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/customer_db
    username: customer_user
    password: secret

application.yml for order-service

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/order_db
    username: order_user
    password: secret

Instead of joining tables across DBs, use API composition.

public class OrderDTO {
    private Order order;
    private Customer customer;
}

// order-service calls customer-service
RestTemplate restTemplate = new RestTemplate();
Customer customer = restTemplate.getForObject("http://customer-service/customers/" + order.getCustomerId(), Customer.class);

✅ Pros and Cons

Pros Cons
Strong service autonomy Harder to perform cross-service joins
Better scalability and resilience Data duplication may occur
Easier versioning and schema changes Eventual consistency instead of strong ACID
Better fault isolation Requires inter-service communication for joins

❌ Anti-Patterns

  • Shared Database Across Services: Defeats microservice independence.
  • Direct DB Access Between Services: Breaks encapsulation and coupling.
  • Too Many DBs for Similar Services: Overhead in setup/maintenance.

🔁 Comparison with Similar Patterns

Pattern Description
Database per Service Each service has its own DB (preferred for autonomy)
Shared Database All services share a common DB (not recommended)
Shared Schema per Service Logical schema isolation, but still shared DB
API Composition Fetch related data via service API, not joins
Event-Driven Sync Services sync using events via Kafka or RabbitMQ

💻 Java Code Example – OrderService Using RestTemplate

@Service
public class OrderService {

    @Autowired
    private RestTemplate restTemplate;

    public OrderDTO getOrderDetails(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        Customer customer = restTemplate.getForObject("http://customer-service/customers/" + order.getCustomerId(), Customer.class);
        return new OrderDTO(order, customer);
    }
}

🔧 Refactoring Legacy Monolith

Before (Monolithic)

SELECT o.id, c.name 
FROM orders o 
JOIN customers c ON o.customer_id = c.id;

After (Microservices with API Composition)

  • order-service fetches order
  • Makes REST call to customer-service for customer details

🌟 Best Practices

  • Use service APIs or events instead of direct DB access.
  • Define clear ownership of each DB.
  • Apply schema versioning per service.
  • Use caching to reduce cross-service calls.
  • Document data contracts clearly.

🧠 Real-World Analogy

Imagine separate departments in a company each maintaining its own records: HR has employee files, Finance has payment records, and IT has access logs. They don’t directly access each other’s files but share summaries via email or reports—just like services using API calls.


☕ Java Feature Relevance

  • Java Records (16+): Lightweight DTOs for composed API responses.
  • Feign Clients: Declarative REST client for inter-service communication.
  • Spring Profiles: Handle different DB credentials/configs per service.

🔚 Conclusion & Key Takeaways

The Database per Service pattern enables scalable, maintainable microservices by enforcing data ownership. While it introduces challenges in querying and consistency, it vastly improves autonomy and resilience.

✅ Summary

  • Isolate each service's data using dedicated databases.
  • Use API composition or eventing to sync data.
  • Avoid direct DB sharing or joins.
  • Monitor and secure each database independently.

❓ FAQ – Database per Service Pattern in Java

1. Why is sharing a database across microservices a bad idea?

It tightly couples services and makes schema evolution difficult.

2. Can services ever access each other’s databases?

No. They should communicate via APIs or events.

3. What database is best for this pattern?

PostgreSQL, MySQL, MongoDB—anything service-scoped.

4. How do I handle joins across services?

Use API composition or CQRS with event sync.

5. Is this suitable for small applications?

It can be overkill—use it when scaling is a requirement.

6. How does security work in this model?

Each DB has its own user/password, roles, and access controls.

7. What’s the alternative to API composition?

Event-driven replication via Kafka or messaging brokers.

8. Is eventual consistency a problem?

Only for critical financial operations—use compensating transactions.

9. Can I still use ORMs like JPA?

Yes. Each service can use JPA with its own datasource.

10. Should I use schema-per-service or database-per-service?

Prefer database-per-service for stronger isolation.