Database Per Service Pattern in Microservices

Each service is responsible for its own data management in a microservices architecture. The Database Per Service Pattern ensures that every microservice has its own dedicated database instead of sharing a single monolithic database. This approach enhances scalability, autonomy, and data consistency across microservices.

This guide will cover everything about the Database Per Service Pattern, including:

  • What it is and why it's important
  • Key benefits and challenges
  • Common implementation strategies
  • Comparison with other database patterns
  • Best practices for implementation
  • A real-world e-commerce example with implementation

1️⃣ What is the Database Per Service Pattern?

The Database Per Service Pattern ensures that each microservice has its own private database. This approach prevents direct database access between services, enforcing better service boundaries and independence.

📌 Key Concept:

  • Each microservice manages its own database.
  • Other microservices cannot directly access another service’s database.
  • Data is synchronized using APIs, events, or messaging systems (Kafka, RabbitMQ, etc.).
  • This pattern eliminates tight coupling between services.

2️⃣ Why Use the Database Per Service Pattern?

Key Benefits

Service Autonomy – Microservices can be developed, deployed, and scaled independently. 

Data Isolation – Prevents one service from affecting another due to database changes. 

Technology Flexibility – Different services can use different databases (SQL, NoSQL, etc.) based on requirements. 

Improved Security – Access control is better as services don’t share a database

Better Performance – Each service optimizes queries for its own workload. 

Decentralized Scalability – Enables independent scaling of databases per service.

3️⃣ Challenges of the Database Per Service Pattern

Potential Issues & Solutions

Challenge Solution
Data Duplication Use event-driven architecture to synchronize data efficiently.
Data Consistency Implement saga pattern or distributed transactions to manage consistency.
Query Complexity Use API composition or CQRS (Command Query Responsibility Segregation) to fetch data from multiple services.
Cross-Service Reporting Use event sourcing or data lakes to gather data for reporting.

4️⃣ Common Implementation Strategies

🔹 1. API Composition (Direct Data Fetching)

Each service exposes APIs, and a client or an API Gateway fetches data from multiple services when needed.

Example: A frontend UI fetching order details calls:

  • GET /orders/{id} (Order Service)
  • GET /products/{id} (Product Service)

🔹 2. Event-Driven Data Synchronization

Services publish events whenever data changes. Other services subscribe to these events and update their local databases accordingly.

Example:

  • Order Service publishes "Order Created" event.
  • Payment Service listens and processes payment.
  • Inventory Service reserves stock based on the event.

🔹 3. CQRS (Command Query Responsibility Segregation)

Separates read and write operations by maintaining separate read and write databases.

  • Write Database: Accepts commands (e.g., POST /orders).
  • Read Database: Stores pre-aggregated data optimized for fast reads.

5️⃣ Database Per Service vs Shared Database

Feature Database Per Service Shared Database
Service Autonomy ✅ High ❌ Low
Data Isolation ✅ High ❌ Low
Technology Choice ✅ Flexible ❌ Limited
Scalability ✅ Independent ❌ Tightly Coupled
Cross-Service Queries ❌ Complex ✅ Easy

6️⃣ Real-World Example: E-Commerce Microservices 🛒

Scenario:

We will implement an e-commerce system with the following microservices, each having its own dedicated database

1️⃣ Product Service – Manages product details (Database: MySQL). 

2️⃣ Order Service – Handles order placement (Database: PostgreSQL). 

3️⃣ Payment Service – Processes payments (Database: MongoDB). 

4️⃣ API Gateway – Routes requests between services.

Step 1: Define Microservices & Databases

Each microservice has its own database for independent data management.

services:
  product-service:
    db: MySQL
  order-service:
    db: PostgreSQL
  payment-service:
    db: MongoDB

Step 2: Create Product Service (with MySQL Database)

Database Configuration (application.properties)

spring.datasource.url=jdbc:mysql://localhost:3306/productdb
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=update
spring.application.name=product-service

Product Model

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;
}

Product Repository

public interface ProductRepository extends JpaRepository<Product, Long> {}

Product Controller

@RestController
@RequestMapping("/products")
public class ProductController {
    @Autowired private ProductRepository productRepository;
    
    @PostMapping
    public Product createProduct(@RequestBody Product product) {
        return productRepository.save(product);
    }
    
    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) {
        return productRepository.findById(id).orElse(null);
    }
}

Step 3: Create Order Service (with PostgreSQL Database)

Database Configuration (application.properties)

spring.datasource.url=jdbc:postgresql://localhost:5432/orderdb
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.hibernate.ddl-auto=update
spring.application.name=order-service

Order Model

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long productId;
    private String productName;
}

Feign Client for Product Service

@FeignClient(name = "product-service")
public interface ProductClient {
    @GetMapping("/products/{id}")
    Product getProductById(@PathVariable Long id);
}

Order Repository

public interface OrderRepository extends JpaRepository<Order, Long> {}

Order Controller

@RestController
@RequestMapping("/orders")
public class OrderController {
    @Autowired private OrderRepository orderRepository;
    @Autowired private ProductClient productClient;
    
    @PostMapping
    public Order createOrder(@RequestBody Order order) {
        Product product = productClient.getProductById(order.getProductId());
        order.setProductName(product.getName());
        return orderRepository.save(order);
    }
}

Step 4: Implement Payment Service (with MongoDB Database)

Database Configuration (application.properties)

spring.data.mongodb.database=paymentdb
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.application.name=payment-service

Payment Model

@Document(collection = "payments")
public class Payment {
    @Id
    private String id;
    private Long orderId;
    private double amount;
}

Payment Repository

public interface PaymentRepository extends MongoRepository<Payment, String> {}

Kafka Event Listener for Order Payments

@Service
public class PaymentService {
    @Autowired private PaymentRepository paymentRepository;
    
    @KafkaListener(topics = "order-events", groupId = "payments")
    public void processPayment(OrderEvent event) {
        Payment payment = new Payment();
        payment.setOrderId(event.getOrderId());
        payment.setAmount(event.getAmount());
        paymentRepository.save(payment);
    }
}

Step 5: Set Up the Eureka Server (Service Registry)

 Create the Eureka Server Project

Use Spring Initializr to create a new project with the following dependency:

  • Eureka Server

 Configure application.properties

server.port=8761
spring.application.name=eureka-server
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false 

 Enable Eureka Server

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

Step 6: API Gateway for Routing Requests

Configuration (application.properties)

server.port=8080
spring.application.name=api-gateway
eureka.client.service-url.default-zone=http://localhost:8761/eureka/
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true

Gateway Routing

spring.cloud.gateway.routes[0].id=product-service
spring.cloud.gateway.routes[0].uri=lb://product-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/products/**

spring.cloud.gateway.routes[1].id=order-service
spring.cloud.gateway.routes[1].uri=lb://order-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/orders/**

spring.cloud.gateway.routes[2].id=payment-service
spring.cloud.gateway.routes[2].uri=lb://payment-service
spring.cloud.gateway.routes[2].predicates[0]=Path=/payments/**

Step 6: Running the Microservices

1️⃣ Start MySQL, PostgreSQL, and MongoDB

2️⃣ Run Eureka Server (eureka-server project)

3️⃣ Start Product Service (product-service project)

4️⃣ Start Order Service (order-service project)

5️⃣ Start Payment Service (payment-service project)

6️⃣ Start API Gateway (api-gateway project).

Step 7: Testing the Implementation

# Create a product
curl -X POST http://localhost:8080/product-service/products -H "Content-Type: application/json" -d '{"name":"Laptop","price":1200}'

# Create an order
curl -X POST http://localhost:8080/order-service/orders -H "Content-Type: application/json" -d '{"productId":1}'

# Verify the payment processing (MongoDB)
mongo
use paymentdb
db.payments.find()

This implementation ensures service autonomy, independent scalability, and data consistency across multiple databases while leveraging Spring Boot, Kafka, and API Gateway.

7️⃣ Best Practices for Implementing Database Per Service

Use Event-Driven Architecture – Synchronize data using Kafka, RabbitMQ, or Event Bus. 

Adopt CQRS – Improve performance by separating read and write models. 

Use API Gateways – Aggregate data from multiple services efficiently. 

Ensure Strong Authentication & Authorization – Secure access to databases. 

Monitor & Optimize Database Performance – Use tools like Prometheus and Grafana. 

Apply Database Sharding & Partitioning – Scale services efficiently.

🎯 Conclusion

The Database Per Service Pattern is crucial for scalable, independent, and secure microservices. It provides autonomy, flexibility, and better performance, though it requires careful data consistency and synchronization mechanisms.

🚀 Key Takeaways:

✔ Each microservice owns its own database

✔ Enables independent scaling and deployment

✔ Requires event-driven communication for data consistency

Best suited for complex, distributed microservices architectures.

Implementing this pattern enhances service resilience, scalability, and maintainability! 🚀

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare