Skip to content

Latest commit

Β 

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Microservices Sample: Clean Architecture with CQRS, Event Sourcing & Saga Choreography

A production-grade sample demonstrating best practices for building microservices using:

  • Clean Architecture - Separation of concerns with clear layers
  • CQRS - Command Query Responsibility Segregation
  • Event Sourcing - Immutable event history as source of truth
  • Saga Choreography - Distributed transactions via event-driven communication
  • TDD - Test-Driven Development with outside-in approach
  • Kafka - Asynchronous inter-service communication

πŸ“‹ Project Overview

Two microservices (Bounded Contexts) collaborate via events to fulfill a business requirement:

Saga: Project Creation Request

β”Œβ”€ User requests to create a project ─┐
β”‚                                     β”‚
β”‚    PROJECT SERVICE (Port 8082)      β”‚
β”‚    β”œβ”€ Creates project (PENDING)     β”‚
β”‚    └─ Publishes: ProjectCreationRequested
β”‚                   β”‚
β”‚                   β”œβ”€β†’ [Kafka Topic] ─→
β”‚                                    β”‚
β”‚    WORKSPACE SERVICE (Port 8081)   β”‚
β”‚    β”œβ”€ Validates quota (max 2)      β”‚
β”‚    β”œβ”€ If quota available:          β”‚
β”‚    β”‚  └─ Publishes: ProjectCreationApproved
β”‚    β”‚                    β”‚
β”‚    β”‚                    └─→ [Kafka Topic] ─→
β”‚    β”‚                                      β”‚
β”‚    β”‚    PROJECT SERVICE                  β”‚
β”‚    β”‚    └─ Updates: Project = ACTIVE    β”‚
β”‚    β”‚                                      β”‚
β”‚    └─ If quota exceeded:           β”‚
β”‚       └─ Publishes: ProjectCreationDenied
β”‚                    β”‚
β”‚                    └─→ [Kafka Topic] ─→
β”‚                                      β”‚
β”‚                    PROJECT SERVICE   β”‚
β”‚                    └─ Updates: Project = REJECTED

πŸ—οΈ Architecture Layers

Clean Architecture Structure

Each Service follows:
β”œβ”€ Presentation Layer (REST Controllers)
β”‚  └─ User-facing HTTP API
β”‚
β”œβ”€ Application Layer (Handlers)
β”‚  └─ Use case orchestration & event handling
β”‚
β”œβ”€ Domain Layer (Aggregates, Events, Repositories)
β”‚  └─ Core business logic (framework-independent)
β”‚
└─ Infrastructure Layer (Repositories, EventBus)
   └─ Database, messaging, external services

Key Design Patterns

  1. Aggregate Pattern - Workspace & Project are aggregates with their own lifecycle
  2. Repository Pattern - Abstraction for data persistence
  3. Event Handler Pattern - Process domain events asynchronously
  4. Saga Pattern - Choreography-based distributed transactions
  5. Service Locator Pattern - Spring dependency injection

πŸ“ Project Structure

microservices-sample/
β”‚
β”œβ”€β”€ shared/                           # Shared infrastructure & events
β”‚   β”œβ”€β”€ src/main/java/
β”‚   β”‚   └── com/workspace/project/shared/
β”‚   β”‚       β”œβ”€β”€ events/               # Domain events (immutable facts)
β”‚   β”‚       β”‚   β”œβ”€β”€ DomainEvent.java
β”‚   β”‚       β”‚   β”œβ”€β”€ ProjectCreationRequested.java
β”‚   β”‚       β”‚   β”œβ”€β”€ ProjectCreationApproved.java
β”‚   β”‚       β”‚   └── ProjectCreationDenied.java
β”‚   β”‚       └── infrastructure/
β”‚   β”‚           └── KafkaEventBus.java
β”‚   └── pom.xml
β”‚
β”œβ”€β”€ workspace-service/                # Workspace BC (Bounded Context)
β”‚   β”œβ”€β”€ src/main/java/
β”‚   β”‚   └── com/workspace/project/workspace/
β”‚   β”‚       β”œβ”€β”€ WorkspaceServiceApplication.java  [Spring Boot App]
β”‚   β”‚       β”œβ”€β”€ domain/
β”‚   β”‚       β”‚   β”œβ”€β”€ Workspace.java                [Aggregate Root]
β”‚   β”‚       β”‚   └── WorkspaceRepository.java      [Repository Interface]
β”‚   β”‚       β”œβ”€β”€ application/
β”‚   β”‚       β”‚   └── ProjectCreationRequestedHandler.java  [Event Handler]
β”‚   β”‚       └── infrastructure/
β”‚   β”‚           └── InMemoryWorkspaceRepository.java
β”‚   β”œβ”€β”€ src/test/java/
β”‚   β”‚   └── ProjectCreationRequestedHandlerTest.java  [TDD Tests]
β”‚   β”œβ”€β”€ src/main/resources/
β”‚   β”‚   └── application.properties
β”‚   └── pom.xml
β”‚
β”œβ”€β”€ project-service/                  # Project BC (Bounded Context)
β”‚   β”œβ”€β”€ src/main/java/
β”‚   β”‚   └── com/workspace/project/project/
β”‚   β”‚       β”œβ”€β”€ ProjectServiceApplication.java    [Spring Boot App]
β”‚   β”‚       β”œβ”€β”€ domain/
β”‚   β”‚       β”‚   β”œβ”€β”€ Project.java                  [Aggregate Root]
β”‚   β”‚       β”‚   └── ProjectRepository.java        [Repository Interface]
β”‚   β”‚       β”œβ”€β”€ application/
β”‚   β”‚       β”‚   β”œβ”€β”€ CreateProjectHandler.java     [Command Handler]
β”‚   β”‚       β”‚   β”œβ”€β”€ CreateProjectCommand.java     [Command DTO]
β”‚   β”‚       β”‚   β”œβ”€β”€ ProjectCreationApprovedHandler.java  [Event Handler]
β”‚   β”‚       β”‚   └── ProjectCreationDeniedHandler.java    [Event Handler]
β”‚   β”‚       β”œβ”€β”€ infrastructure/
β”‚   β”‚       β”‚   └── InMemoryProjectRepository.java
β”‚   β”‚       └── presentation/
β”‚   β”‚           └── ProjectController.java        [REST API]
β”‚   β”œβ”€β”€ src/test/java/
β”‚   β”‚   └── ProjectSagaHandlersTest.java  [TDD Tests]
β”‚   β”œβ”€β”€ src/main/resources/
β”‚   β”‚   └── application.properties
β”‚   └── pom.xml
β”‚
β”œβ”€β”€ docker-compose.yml                # Kafka infrastructure
β”œβ”€β”€ pom.xml                           # Parent POM (multi-module)
└── README.md                         # This file

πŸš€ Quick Start

Prerequisites

  • Java 17+
  • Maven 3.8+
  • Docker & Docker Compose

1. Start Kafka Infrastructure

docker-compose up -d

Verify Kafka is running:

2. Build All Modules

mvn clean package -DskipTests

3. Run Services

Terminal 1 - Workspace Service:

cd workspace-service
java -jar target/workspace-service-1.0-SNAPSHOT.jar

Terminal 2 - Project Service:

cd project-service
java -jar target/project-service-1.0-SNAPSHOT.jar

4. Run Tests

# Test all modules
mvn clean test

# Test specific module
mvn test -pl workspace-service
mvn test -pl project-service

πŸ§ͺ Testing with Postman

Test Case 1: User with Quota (Approval)

User 001: Has 0 projects, can create 2 more.

POST http://localhost:8082/api/projects
Content-Type: application/json

{
  "userId": "user-001",
  "name": "My First Project",
  "description": "Building something amazing"
}

Response (201 Created):

{
  "projectId": "proj-abc123",
  "message": "Project created, awaiting approval"
}

Wait 2 seconds for saga to complete, then check status:

GET http://localhost:8082/api/projects/proj-abc123

Expected Response:

{
  "projectId": "proj-abc123",
  "userId": "user-001",
  "name": "My First Project",
  "description": "Building something amazing",
  "status": "ACTIVE"
}

Test Case 2: User at Quota (Denial)

User 003: Has 2 projects (at limit).

POST http://localhost:8082/api/projects
Content-Type: application/json

{
  "userId": "user-003",
  "name": "Third Project",
  "description": "Should be rejected"
}

Check status after 2 seconds:

GET http://localhost:8082/api/projects/proj-xyz789

Expected Response:

{
  "projectId": "proj-xyz789",
  "userId": "user-003",
  "name": "Third Project",
  "description": "Should be rejected",
  "status": "REJECTED"
}

Test Case 3: Watch Events in Kafka UI

  1. Open http://localhost:8080 (Kafka UI)
  2. Click Topics
  3. View messages in:
    • ProjectCreationRequested - requests from Project Service
    • ProjectCreationApproved - approvals from Workspace Service
    • ProjectCreationDenied - denials from Workspace Service

πŸ“Š Event Flow Example

Successful Project Creation

1. [Client] POST /api/projects {userId: user-001, name: "My Project"}
                ↓
2. [Project Service] CreateProjectHandler
   - Creates Project aggregate (status: PENDING_APPROVAL)
   - Saves to repository
   - Publishes: ProjectCreationRequested
                ↓ [Kafka]
3. [Workspace Service] ProjectCreationRequestedHandler
   - Loads Workspace(user-001) β†’ has 0 projects
   - Checks: canCreateProject() β†’ true
   - Increments: projectCount = 1
   - Saves Workspace
   - Publishes: ProjectCreationApproved
                ↓ [Kafka]
4. [Project Service] ProjectCreationApprovedHandler
   - Loads Project aggregate
   - Calls: project.approve()
   - Updates status: PENDING_APPROVAL β†’ ACTIVE
   - Saves Project
                ↓
5. [Client] GET /api/projects/{id}
   Response: {"status": "ACTIVE", ...}

🧬 TDD Approach: Outside-In

This project demonstrates Test-Driven Development with the outside-in strategy:

1. Write Failing Tests

@Test
void shouldApproveProjectWhenUserHasQuota() {
    // Arrange: Create mock workspace with 0 projects
    Workspace workspace = new Workspace("ws-001", "user-001", 0);
    when(repository.findByUserId("user-001")).thenReturn(Optional.of(workspace));

    ProjectCreationRequested event = new ProjectCreationRequested(...);

    // Act
    handler.handle(event);

    // Assert: ProjectCreationApproved should be published
    verify(eventBus).publish(ArgumentMatchers.any(ProjectCreationApproved.class));
}

2. Implement Minimal Code

public class ProjectCreationRequestedHandler {
    public void handle(DomainEvent event) {
        Workspace workspace = workspaceRepository.findByUserId(userId).get();

        if (workspace.canCreateProject()) {
            workspace.incrementProjectCount();
            workspaceRepository.save(workspace);
            eventBus.publish(new ProjectCreationApproved(...));
        }
    }
}

3. Refactor with Confidence

Tests remain unchanged while implementation improves.

πŸ”‘ Key Concepts

Clean Architecture Benefits

βœ… Testability - Domain logic has no framework dependencies βœ… Maintainability - Clear separation of concerns βœ… Scalability - Easy to add new use cases βœ… Flexibility - Swap implementations (DB, messaging, etc.)

CQRS Benefits

βœ… Performance - Optimize reads and writes separately βœ… Clarity - Commands (intentions) vs Events (facts) βœ… Flexibility - Different data models for different concerns

Event Sourcing Benefits

βœ… Audit Trail - Complete history of all changes βœ… Debugging - Replay events to understand state βœ… Temporal Queries - Query state at any point in time βœ… Eventual Consistency - Natural fit for distributed systems

Saga Choreography Benefits

βœ… Loose Coupling - Services don't call each other directly βœ… Asynchronous - Non-blocking inter-service communication βœ… Scalable - Services can scale independently βœ… Resilient - Failure in one service doesn't stop others

πŸ”§ Configuration

Workspace Service Quotas

Edit /workspace-service/src/main/java/com/workspace/project/workspace/domain/Workspace.java:

private static final int MAX_PROJECTS = 2;  // Change this value

Pre-populated Test Data

Edit /workspace-service/src/main/java/.../WorkspaceServiceApplication.java:

@Bean
public WorkspaceRepository workspaceRepository() {
    repo.save(new Workspace("ws-001", "user-001", 0));  // user-001: 0 projects
    repo.save(new Workspace("ws-002", "user-002", 1));  // user-002: 1 project
    repo.save(new Workspace("ws-003", "user-003", 2));  // user-003: 2 projects
}

πŸ“ˆ Extending the System

Add a Notification Service

# Create new module
mkdir notification-service

# Subscribe to ProjectCreationApproved
public void handle(ProjectCreationApproved event) {
    // Send email/SMS to user
    notificationService.sendEmail(event.getUserId(), ...);
}

Add a Reporting Service

# Create new module
mkdir reporting-service

# Subscribe to all events
// Builds analytics database
// Provides reporting API

Switch to Database

Replace InMemoryWorkspaceRepository with PostgresWorkspaceRepository:

@Repository
public class PostgresWorkspaceRepository implements WorkspaceRepository {
    @Autowired
    private JpaRepository<WorkspaceEntity, String> jpaRepo;

    @Override
    public Optional<Workspace> findByUserId(String userId) {
        return jpaRepo.findByUserId(userId)
            .map(entity -> new Workspace(...));
    }
}

πŸ› Troubleshooting

Kafka Connection Error

# Check if Kafka is running
docker ps | grep kafka

# Restart Kafka
docker-compose down
docker-compose up -d

Port Already in Use

# Kill process on port 8081
lsof -ti:8081 | xargs kill -9

# Kill process on port 8082
lsof -ti:8082 | xargs kill -9

Events Not Appearing

  1. Verify Kafka is running: docker ps
  2. Check service logs for errors
  3. Verify topics exist in Kafka UI (http://localhost:8080)
  4. Ensure both services are running

Tests Failing

# Run with verbose output
mvn test -X

# Run specific test
mvn test -Dtest=ProjectCreationRequestedHandlerTest

πŸ“š Further Reading

πŸ“ Summary

This sample demonstrates:

βœ… Clean Architecture with distinct layers βœ… CQRS separating commands from queries βœ… Event Sourcing using Kafka as event store βœ… Saga Choreography for distributed transactions βœ… TDD with outside-in test approach βœ… Spring Boot for rapid service development βœ… Kafka for asynchronous messaging βœ… Production patterns ready for real systems

Use this as a template for your microservices projects!


Ready to run? Execute:

docker-compose up -d
mvn clean package -DskipTests
# Terminal 1
cd workspace-service && java -jar target/*.jar
# Terminal 2
cd project-service && java -jar target/*.jar

Then test with Postman following the examples above! πŸš€

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages