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
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
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
- Aggregate Pattern - Workspace & Project are aggregates with their own lifecycle
- Repository Pattern - Abstraction for data persistence
- Event Handler Pattern - Process domain events asynchronously
- Saga Pattern - Choreography-based distributed transactions
- Service Locator Pattern - Spring dependency injection
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
- Java 17+
- Maven 3.8+
- Docker & Docker Compose
docker-compose up -dVerify Kafka is running:
- Kafka UI: http://localhost:8080
mvn clean package -DskipTestsTerminal 1 - Workspace Service:
cd workspace-service
java -jar target/workspace-service-1.0-SNAPSHOT.jar- Runs on http://localhost:8081
- Listens for
ProjectCreationRequestedevents
Terminal 2 - Project Service:
cd project-service
java -jar target/project-service-1.0-SNAPSHOT.jar- Runs on http://localhost:8082
- Listens for
ProjectCreationApprovedandProjectCreationDeniedevents
# Test all modules
mvn clean test
# Test specific module
mvn test -pl workspace-service
mvn test -pl project-serviceUser 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-abc123Expected Response:
{
"projectId": "proj-abc123",
"userId": "user-001",
"name": "My First Project",
"description": "Building something amazing",
"status": "ACTIVE"
}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-xyz789Expected Response:
{
"projectId": "proj-xyz789",
"userId": "user-003",
"name": "Third Project",
"description": "Should be rejected",
"status": "REJECTED"
}- Open http://localhost:8080 (Kafka UI)
- Click Topics
- View messages in:
ProjectCreationRequested- requests from Project ServiceProjectCreationApproved- approvals from Workspace ServiceProjectCreationDenied- denials from Workspace Service
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", ...}
This project demonstrates Test-Driven Development with the outside-in strategy:
@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));
}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(...));
}
}
}Tests remain unchanged while implementation improves.
β 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.)
β Performance - Optimize reads and writes separately β Clarity - Commands (intentions) vs Events (facts) β Flexibility - Different data models for different concerns
β 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
β 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
Edit /workspace-service/src/main/java/com/workspace/project/workspace/domain/Workspace.java:
private static final int MAX_PROJECTS = 2; // Change this valueEdit /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
}# Create new module
mkdir notification-service
# Subscribe to ProjectCreationApproved
public void handle(ProjectCreationApproved event) {
// Send email/SMS to user
notificationService.sendEmail(event.getUserId(), ...);
}# Create new module
mkdir reporting-service
# Subscribe to all events
// Builds analytics database
// Provides reporting APIReplace 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(...));
}
}# Check if Kafka is running
docker ps | grep kafka
# Restart Kafka
docker-compose down
docker-compose up -d# Kill process on port 8081
lsof -ti:8081 | xargs kill -9
# Kill process on port 8082
lsof -ti:8082 | xargs kill -9- Verify Kafka is running:
docker ps - Check service logs for errors
- Verify topics exist in Kafka UI (http://localhost:8080)
- Ensure both services are running
# Run with verbose output
mvn test -X
# Run specific test
mvn test -Dtest=ProjectCreationRequestedHandlerTest- Clean Architecture - Robert C. Martin
- CQRS Pattern - Microsoft Docs
- Event Sourcing - Martin Fowler
- Saga Pattern - Chris Richardson
- TDD by Example - Kent Beck
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/*.jarThen test with Postman following the examples above! π