Thursday, May 15, 2025

Microservice Design Patterns

 1. Service Registry

A service registry is like a map for your services; it keeps track of all the services in your system, making it easier for them to find each other.

Every service in your system needs to register itself with the service registry when it starts up, and deregister when it shuts down. Other services can then query the service registry to locate the services they need to interact with. This allows your system to be dynamic and adaptable, as services can come and go as required without disrupting the overall functionality.

Purpose: Enables dynamic discovery of services in a distributed system. 

Key Tooling: Netflix Eureka, Consul, Apache Zookeeper. 

Benefit: Decouples services from hardcoded addresses—essential for scalability and resilience in cloud-native environments.

2. Circuit Breaker

A circuit breaker is used to detect failures and encapsulate the logic of preventing a failure from constantly recurring. Circuit breakers could be triggered due to bugs in one or more microservices, temporary external system failure, or unexpected operating conditions. 

In a microservices architecture, you employ the circuit breaker pattern to monitor the interaction between services. If a service is failing or responding slowly, the circuit breaker trips and prevents further calls to the service, thus preventing a system-wide failure. Once the service is back up, the circuit breaker resets, and things go back to normal.

Purpose: Prevents repeated calls to a failing service and allows graceful recovery. 

Key Tooling: Resilience4j, Hystrix (deprecated), Spring Cloud Circuit Breaker.

Phases:

Closed: Normal operation.

Open: Stops calls to failing service.

Half-Open: Tests if the service has recovered.

3. API Gateway

An API gateway acts as a single entry point into your system for all clients. This can be especially beneficial if you have multiple client apps, such as a web app and a mobile app, as it allows you to maintain a single API for all clients, simplifying client-side code. 

The API gateway can handle requests in one of two ways. It could route requests to the appropriate services directly, or it could use a process known as composition, where it would combine data from multiple services and return the aggregate result to the client. This not only simplifies client-side code but also makes your system more efficient and user-friendly.

Purpose: Acts as a single entry point for all client requests. 

Responsibilities

Request routing.

Authentication & rate limiting.

Aggregation of responses.

Key Tooling: Spring Cloud Gateway, Kong, NGINX, AWS API Gateway.

4. Event-Driven Architecture

In an event-driven architecture, when a service performs an action that other services need to know about, it emits an event—a record of the action. Other services then react to the event as necessary. This is a powerful way to decouple services and allows for highly scalable and robust systems. 

This architecture allows you to build systems that are more resilient to failure, as the services do not need to be aware of each other. If one service fails, it does not affect the others. Additionally, this architecture allows for high scalability, as you can add new services to the system without affecting existing ones.

Purpose: Promotes loose coupling through asynchronous communication via events. 

Key Tooling: Kafka, RabbitMQ, NATS. 

Benefits: High scalability, resilience, and flexibility to evolve. 

5. Database per Service

In a traditional monolithic application, you would have a single database that all services interact with. However, in a microservices architecture, each service has its own database. 

Why is this beneficial? Well, it allows each service to be decoupled from the others, which means that a failure in one service does not affect the others. Furthermore, it allows for better performance, as each service can be optimized independently based on its specific needs.

Purpose: Ensures service autonomy by giving each microservice its own database. 

Pros: 

Decoupled schema evolution.

Data isolation.

Cons: Requires managing data consistency across services (e.g., using Saga). 

6. Command Query Responsibility Segregation (CQRS)

CQRS is a microservices design pattern that separates read and write operations. In traditional systems, the same data model is often used for both these operations. However, CQRS advocates for a different approach. It proposes the use of separate models for update (Command) and read (Query) operations. This segregation enables you to optimize each model for its specific purpose, thereby improving performance and scalability. 

However, implementing CQRS is not without its challenges. It can complicate your system due to the need to synchronize two data models. But, when applied correctly, it can significantly enhance the flexibility and performance of your system.

Purpose: Separates models for reading and writing data. 

Benefits:  

Optimized performance.

Tailored data access patterns.

Commonly Paired With: Event Sourcing, for auditability and replayability.

7. Externalized Configuration

The externalized configuration pattern advocates for the separation of configuration from the code. This separation allows you to modify the behavior of your application without the need for code changes or system restarts. 

This pattern is particularly useful in microservices architectures where you may have multiple instances of a service running with different configurations. By externalizing the configuration, you can manage all instances efficiently. However, it does require a robust configuration management system to avoid configuration drift.

Purpose: Manages environment-specific configurations outside the codebase. 

Key Tooling: Spring Cloud Config, HashiCorp Vault, Consul.

Benefits: Dynamic updates without redeployments, centralized config management.

8. Saga Pattern

The saga pattern is used to ensure data consistency across multiple services in a microservices architecture. In traditional monolithic systems, transactions are usually managed using a two-phase commit. However, in a microservices architecture, where services are loosely coupled and distributed, this approach is not practical. 

The saga pattern proposes an alternative solution. It suggests breaking a transaction into multiple local transactions. Each local transaction updates data within a single service and publishes an event. Other services listen to these events and perform their local transactions. If a local transaction fails, compensating transactions are executed to undo the changes.

Purpose: Manages distributed transactions via a sequence of local transactions with compensation logic. 

Implementation Types: 

Choreography: Services communicate via events without a central controller.

Orchestration: A central coordinator dictates the transaction steps.

Use Case: Booking systems, e-commerce order management.

9. Bulkhead Pattern

The bulkhead pattern is a microservices design pattern that helps to prevent failures in one part of a system from cascading to other parts. It does so by isolating elements of an application into pools so that if one fails, the others continue to function. 

This pattern is inspired by the bulkheads in a ship. Just as a ship is divided into watertight compartments to prevent it from sinking if one part is breached, an application can be divided into isolated groups to protect it from failures.

Purpose: Isolates different service components to contain failures. 

Analogy: Like a ship’s compartments preventing total sinking from a breach. 

Common Uses: Thread pools, circuit breakers per service/functionality.

10. Backends for Frontends (BFF)

The BFF pattern proposes the creation of separate backend services for different types of clients (like desktop, mobile, etc.). This allows you to tailor the backend services to the specific needs of each client, thereby improving user experience and performance.

However, this pattern can lead to code duplication if not managed properly. Therefore, it is crucial to strike a balance between customization and code reuse when using the BFF pattern.

Purpose: Tailors APIs for specific front-end needs (e.g., mobile vs. web). 

Benefits: 

Optimized performance and UX.

Cleaner, client-specific logic.

Challenge: Avoiding excessive code duplication

Sample Microservice Application: -

Using these technologies Java 17, Spring Boot 3.2, Spring Data JPA

H2 / PostgreSQL, Validation (javax/hibernate), ResponseEntity , DTO mapping

๐Ÿงฑ Project Structure

employee-service/

├── src/

│   └── main/

│       ├── java/

│       │   └── com/

│       │       └── example/

│       │           └── employeeservice/

│       │               ├── controller/

│       │               │   └── EmployeeController.java

│       │               ├── dto/

│       │               │   ├── EmployeeRequestDTO.java

│       │               │   └── EmployeeResponseDTO.java

│       │               ├── entity/

│       │               │   └── Employee.java

│       │               ├── exception/

│       │               │   └── GlobalExceptionHandler.java

│       │               ├── repository/

│       │               │   └── EmployeeRepository.java

│       │               ├── service/

│       │               │   ├── EmployeeService.java

│       │               │   └── impl/

│       │               │       └── EmployeeServiceImpl.java

│       │               └── EmployeeServiceApplication.java

│       └── resources/

│           └── application.properties

└── pom.xml

๐Ÿงฉ Key Technologies

Java 17

Spring Boot 3.2

Spring Data JPA

H2 / PostgreSQL

Validation (javax/hibernate)

ResponseEntity

DTO mapping

๐Ÿง‘‍๐Ÿ’ป Entity: Employee.java 

package com.example.employeeservice.entity;

import jakarta.persistence.*;

import jakarta.validation.constraints.Email;

import lombok.*; 

@Entity

@Table(name = "employees", uniqueConstraints = @UniqueConstraint(columnNames = "email"))

@Getter

@Setter

@NoArgsConstructor

@AllArgsConstructor

@Builder

public class Employee {

    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;

    @Column(nullable = false)

    private String name;

    @Email

    @Column(nullable = false, unique = true)

    private String email;

    private String department;

}

✅ DTOs

EmployeeRequestDTO.java

package com.example.employeeservice.dto;

import jakarta.validation.constraints.Email;

import jakarta.validation.constraints.NotBlank;

public record EmployeeRequestDTO(

    @NotBlank(message = "Name is required") String name,

    @Email(message = "Invalid email format") String email,

    String department

) {}

EmployeeResponseDTO.java

package com.example.employeeservice.dto;

public record EmployeeResponseDTO(

    Long id,

    String name,

    String email,

    String department

) {}

๐Ÿ› ️ Service Layer: EmployeeService.java

package com.example.employeeservice.service;

import com.example.employeeservice.dto.EmployeeRequestDTO;

import com.example.employeeservice.dto.EmployeeResponseDTO;

import java.util.List;

public interface EmployeeService {

    EmployeeResponseDTO create(EmployeeRequestDTO dto);

    EmployeeResponseDTO update(Long id, EmployeeRequestDTO dto);

    void delete(Long id);

    EmployeeResponseDTO getById(Long id);

    List<EmployeeResponseDTO> getAll();

}

EmployeeServiceImpl.java

package com.example.employeeservice.service.impl;

import com.example.employeeservice.dto.*;

import com.example.employeeservice.entity.Employee;

import com.example.employeeservice.repository.EmployeeRepository;

import com.example.employeeservice.service.EmployeeService;

import jakarta.persistence.EntityNotFoundException;

import lombok.RequiredArgsConstructor;

import org.springframework.stereotype.Service;

import java.util.List;

@Service

@RequiredArgsConstructor

public class EmployeeServiceImpl implements EmployeeService {

    private final EmployeeRepository repository;

    @Override

    public EmployeeResponseDTO create(EmployeeRequestDTO dto) {

        Employee employee = Employee.builder()

                .name(dto.name())

                .email(dto.email())

                .department(dto.department())

                .build();

        return mapToDTO(repository.save(employee));

    } 

    @Override

    public EmployeeResponseDTO update(Long id, EmployeeRequestDTO dto) {

        Employee emp = repository.findById(id)

                .orElseThrow(() -> new EntityNotFoundException("Employee not found"));

        emp.setName(dto.name());

        emp.setEmail(dto.email());

        emp.setDepartment(dto.department());

        return mapToDTO(repository.save(emp));

    }

    @Override

    public void delete(Long id) {

        repository.deleteById(id);

    }

    @Override

    public EmployeeResponseDTO getById(Long id) {

        return repository.findById(id)

                .map(this::mapToDTO)

                .orElseThrow(() -> new EntityNotFoundException("Employee not found"));

    }

    @Override

    public List<EmployeeResponseDTO> getAll() {

        return repository.findAll().stream().map(this::mapToDTO).toList();

    }

    private EmployeeResponseDTO mapToDTO(Employee e) {

        return new EmployeeResponseDTO(e.getId(), e.getName(), e.getEmail(), e.getDepartment());

    }

}

๐ŸŒ Controller: EmployeeController.java

package com.example.employeeservice.controller;

import com.example.employeeservice.dto.*;

import com.example.employeeservice.service.EmployeeService;

import jakarta.validation.Valid;

import lombok.RequiredArgsConstructor;

import org.springframework.http.*;

import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController

@RequestMapping("/api/employees")

@RequiredArgsConstructor

public class EmployeeController {

    private final EmployeeService employeeService;

    @PostMapping

    public ResponseEntity<EmployeeResponseDTO> create(@Valid @RequestBody EmployeeRequestDTO dto) {

        return new ResponseEntity<>(employeeService.create(dto), HttpStatus.CREATED);

    }

    @PutMapping("/{id}")

    public ResponseEntity<EmployeeResponseDTO> update(@PathVariable Long id, @Valid @RequestBody EmployeeRequestDTO dto) {

        return ResponseEntity.ok(employeeService.update(id, dto));

    }

    @DeleteMapping("/{id}")

    public ResponseEntity<Void> delete(@PathVariable Long id) {

        employeeService.delete(id);

        return ResponseEntity.noContent().build();

    }

    @GetMapping("/{id}")

    public ResponseEntity<EmployeeResponseDTO> getById(@PathVariable Long id) {

        return ResponseEntity.ok(employeeService.getById(id));

    }

    @GetMapping

    public ResponseEntity<List<EmployeeResponseDTO>> getAll() {

        return ResponseEntity.ok(employeeService.getAll());

    }

}

๐Ÿงพ Repository

package com.example.employeeservice.repository;

import com.example.employeeservice.entity.Employee;

import org.springframework.data.jpa.repository.JpaRepository;

public interface EmployeeRepository extends JpaRepository<Employee, Long> {}

⚠️ Exception Handling

package com.example.employeeservice.exception;

import jakarta.persistence.EntityNotFoundException;

import org.springframework.http.*;

import org.springframework.web.bind.MethodArgumentNotValidException;

import org.springframework.web.bind.annotation.*;

import java.util.Map;

import java.util.stream.Collectors;

@RestControllerAdvice

public class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)

    public ResponseEntity<Map<String, String>> handleNotFound(EntityNotFoundException ex) {

        return ResponseEntity.status(HttpStatus.NOT_FOUND)

                .body(Map.of("error", ex.getMessage()));

    }

    @ExceptionHandler(MethodArgumentNotValidException.class)

    public ResponseEntity<Map<String, String>> handleValidation(MethodArgumentNotValidException ex) {

        Map<String, String> errors = ex.getBindingResult()

                .getFieldErrors()

                .stream()

                .collect(Collectors.toMap(f -> f.getField(), f -> f.getDefaultMessage()));

        return ResponseEntity.badRequest().body(errors);

    }

}

๐Ÿงช Example application.properties

spring.datasource.url=jdbc:h2:mem:employeedb

spring.datasource.driverClassName=org.h2.Driver

spring.datasource.username=sa

spring.datasource.password=

spring.h2.console.enabled=true

spring.jpa.hibernate.ddl-auto=update


No comments:

Post a Comment

Java 9 and Java11 and Java17, Java 21 Features

 Java 9 and Java11 and Java17 features along with explanation and examples in realtime scenarios Here's a detailed breakdown of Java 9, ...