A microservices-based architecture for Project A, following Domain-Driven Design (DDD) principles, Hexagonal Architecture (Ports & Adapters), Separation of Concerns (SoC), SOLID principles, Choreography pattern for service coordination, and the Saga pattern for distributed transactions.
The project is split into the following components:
- API Gateway – Entry point for all client requests, handles routing to appropriate services and JWT token validation
- IAM Service – Consolidates authentication, user management, and roles/permissions into a single service (port 8082)
- Mail Service – Handles email sending operations and templates (port 8085)
- Shared Infrastructure – Shared infrastructure, contracts, and utilities used across all microservices
- Template Service – Baseline configuration for future microservices
Each microservice follows Hexagonal Architecture principles with a three-layer structure and has its own PostgreSQL database. Services communicate with each other via Apache Kafka for event-driven architecture, primarily for notifying the Mail Service.
- The system uses Kafka in KRaft mode (Kafka Raft), eliminating the need for Zookeeper.
- Configuration is handled via
KAFKA_PROCESS_ROLES(broker, controller) andKAFKA_CONTROLLER_QUORUM_VOTERS. - A static
CLUSTER_IDis provided indocker-compose.ymlfor simplified setup.
- Provides shared code, DTOs, event definitions, and utilities for all microservices
- Implements shared patterns like an Outbox pattern
- Contains reusable components for Kafka integration, exception handling, and API responses
- Ensures consistency in how services communicate and process events
- Includes base classes for implementing event choreography
- Provides shared ports and adapters interfaces for consistent hexagonal architecture implementation
- Integration events definitions for inter-service communication
- Responsible for user authentication, authorization, profile management, and roles.
- Uses JWT and refresh tokens (http-only secure cookies) for session management.
- Database stores:
- User credentials (email, hashed passwords)
- User profile data (first name, last name, preferences)
- Role definitions and user-role assignments
- Refresh tokens and verification tokens
- Provides endpoints for registration, login, profile management, and role administration.
- Publishes mail request events to trigger email workflows.
- Implements an internal Saga pattern for multi-step operations like user registration.
- Responsible for sending emails based on templates
- Database stores:
- Email logs
- Delivery status
- Consumes mail request events from other services
- Supports various email templates (welcome, password reset, account activation, etc.)
- Acts as a reactive service in the choreography flow
- Uses hexagonal architecture to decouple email sending logic from external mail providers
- A template for implementing hexagonal architecture
- Example domain models, repositories, and services
- Sample event definitions and Kafka integration
- Basic Saga pattern implementation
- Back-end: Spring Boot, Kotlin, Gradle Kotlin DSL
- Database: PostgreSQL (a separate instance for each service)
- Message Broker: Apache Kafka KRaft (Zookeeper-less)
- API Documentation: OpenAPI (Swagger)
- Containerization: Docker/Podman, Docker Compose/Podman Compose
- Authentication: JWT and refresh tokens (http only secure cookie)
- Testing: JUnit, Testcontainers
- Docker and Docker Compose / Podman and Podman Compose
- JDK
- Gradle
- Clone the repository:
git clone https://github.com/vertyll/veds.git
# and
cd veds- Start the infrastructure:
docker-compose up -d- Build and run microservices: Each microservice is now an independent Gradle project. To build or run a specific service, navigate to its directory:
Building:
cd <service-name>
./gradlew buildLocal Running:
cd <service-name>
./gradlew bootRun --args='--spring.profiles.active=dev'Available Services:
api-gatewayiam-servicemail-serviceshared-infrastructure(library)
- Access the services:
- API Gateway: http://localhost:8080
- IAM Service: http://localhost:8082
- Mail Service: http://localhost:8083
- Kafka UI: http://localhost:8090
- MailDev: http://localhost:1080
For convenience, you can use the provided Makefile in the root directory:
- Build all services:
make build-all - Run tests in all services:
make test-all - Clean all services:
make clean-all
- Make changes to the relevant service code
- Build the service:
cd <service-name>
./gradlew build- Start the microservice:
./gradlew bootRun --args='--spring.profiles.active=dev'Each microservice implements Hexagonal Architecture to achieve clean separation of concerns.
Benefits:
- Testability: Core business logic can be tested independently
- Flexibility: Easy to swap external dependencies without affecting business logic
- Maintainability: Clear separation between business rules and technical details
- Technology Independence: Core domain is not coupled to specific frameworks or technologies
Each microservice is designed around a specific business domain with:
- A clear bounded context
- Domain models that represent business entities
- A layered architecture within the hexagonal structure
- Domain-specific language
- Encapsulated business logic
The project structure follows DDD principles within hexagonal architecture:
domain: Core business models, domain services, domain events, and business logicapplication: Controllers, DTOs, use cases, application services, and port definitions (Primary Adapters)infrastructure: Adapters for external systems like databases, Kafka, email providers, etc. (Secondary Adapters)
The codebase adheres to:
- Single Responsibility Principle: Each class has a single responsibility
- Open/Closed Principle: Classes are open for extension but closed for modification
- Liskov Substitution Principle: Subtypes are substitutable for their base types
- Interface Segregation Principle: Specific interfaces rather than general ones
- Dependency Inversion Principle: Depends on abstractions (ports), not concretions (adapters)
The hexagonal architecture naturally enforces these principles by:
- Isolating business logic from external concerns
- Using dependency inversion through ports and adapters
- Maintaining clear boundaries between layers
The system uses a choreography-based approach for service coordination:
- Services react to events published by other services without central coordination
- Each service knows which events to listen for and what actions to take
- No central orchestrator is needed, making the system more decentralized and resilient
- Services maintain autonomy and can evolve independently
Benefits of this approach:
- Reduced coupling between services
- More flexible and scalable architecture
- Easier to add new services or modify existing ones
- Better resilience as there's no single point of failure
- Aligns well with hexagonal architecture by treating event communication as external adapters
For distributed transactions, we use the Saga pattern (currently internal to IAM Service but can be extended):
- A service or component initiates a multistep operation (e.g., registration).
- Each step is recorded in the Saga state machine.
- If a step fails, compensating transactions are triggered to maintain consistency.
Example: User Registration Saga (Internal to IAM Service)
- Create User: Creates the user record.
- Create Verification Token: Generates a token for account activation.
- Send Welcome Email: Publishes a
MailRequestedEventto Kafka for the Mail Service.
Each step is handled through hexagonal architecture, ensuring clean separation between the domain logic and persistence/event adapters.
Services communicate asynchronously through Kafka events:
- UserActivatedEvent: Triggered when a user activates their account.
- MailRequestedEvent: Triggered when an email needs to be sent (consumed by Mail Service).
- SagaCompensationEvent: Internal event for triggering compensation logic.
Event publishing and consuming are handled by dedicated adapters, keeping the domain core focused on business logic.
This project uses a combination of HTTP ETags (at API boundaries) and JPA Optimistic Locking (within services) to prevent lost updates and to ensure safe concurrency across microservices and asynchronous processing.
- API layer: ETag/If-Match for conditional updates from clients (front-end, API consumers).
- Persistence layer: JPA
@Versionon entities and the load → mutate → save a pattern inside a single transaction. - Sagas and Outbox: Internal consistency ensured by JPA Optimistic Locking and idempotency safeguards — no HTTP ETags here.
- IAM Service
PUT /users/{id}(Update profile) – returns and optionally acceptsIf-Match.POST /roles/user/{userId}/role/{roleName}(Assign role) andDELETE /roles/user/{userId}/role/{roleName}(Remove a role) optionally acceptIf-Matchfor the User entity.GET /roles/{id},GET /roles/name/{name}returnETag: W/"<version>".GET /users/{id},GET /users/email/{email}returnETag: W/"<version>"for clients that want to track staleness.
Example client flow (Profile update):
- Read current state
curl -i http://localhost:8082/users/1
# Response contains: ETag: W/"<version>"
- Update with precondition
curl -i -X PUT http://localhost:8082/users/1 \
-H 'Content-Type: application/json' \
-H 'If-Match: W/"<version>"' \
-d '{"firstName": "John", "lastName": "Doe", "profilePicture": "htt...", "phoneNumber": "123456789", "address": "123 St"}'
- If the resource changed meanwhile, the server returns
412.
Error semantics at the API layer:
428 Precondition Required— missingIf-Matchon required endpoints.412 Precondition Failed— ETag/If-Match does not match the current version.409 Conflict— last-resort handler for JPAObjectOptimisticLockingFailureException(race detected at commit time).
- Entities use
@Versionto enable Optimistic Locking (e.g.,User,Role,KafkaOutbox, and Saga/SagaStep entities where applicable). - Services follow the pattern: load the entity → apply changes →
save(...)inside@Transactional. Hibernate includesWHERE version = ?and raises a conflict if data changed concurrently.
- Sagas are backend-internal processes (event-driven), not HTTP resources — therefore ETag/If-Match is not used in Sagas.
- Concurrency control:
@Versionon Saga and/or SagaStep where applicable, persisted via a load → mutate → save.- Idempotency for steps:
- Database-level unique constraint on
(sagaId, stepName)prevents duplicate step insertion. - Service-level soft check in
recordSagaStepreturns the existing step if already present (safe retries/duplicates).
- Database-level unique constraint on
- Compensation steps are recorded and published through Outbox when needed.
KafkaOutboxhas@Version.- The
KafkaOutboxProcessorupdates message status by mutating the loaded entity and callingsave(...)(no bulk JPQL updates). This leverages Optimistic Locking to avoid races between processor instances. - On failure, the processor increments
retryCount, stores the error, and persists viasave(...)again.
Each service provides its own Swagger UI for API documentation:
- IAM Service: http://localhost:8082/swagger-ui.html
- Mail Service: http://localhost:8083/swagger-ui.html
- Each service exposes health and metrics endpoints through Spring Boot Actuator
- Health checks can be accessed at
/actuator/healthon each service - Metrics can be collected for observability and monitoring service health
The project uses ktlint for code formatting and style checks. Go to the service directory and run:
./gradlew ktlintFormatTo check the code style, run:
./gradlew ktlintCheckYou can also use make format-all and make check-style-all from the root directory.
