Dependencies flow inward only:
Adapter Layer → Application Layer → Domain Layer
- Domain has NO dependencies (pure Java)
- Application depends only on Domain
- Adapters depend on Application and Domain
- Infrastructure wires everything together
Location: domain/
Contains:
- Entities with business rules
- Domain services for complex logic
- Value objects and enums
Characteristics:
- No framework dependencies
- Pure POJOs and business logic
- Can be tested without any infrastructure
// Domain entity with business rules
public class Payment {
private String id;
private BigDecimal amount;
public boolean canBeCancelled() {
return status == PaymentStatus.PENDING;
}
}
// Domain service for complex business rules
public class PaymentDomainService {
public PaymentValidationResult validatePayment(Payment payment) {
// Pure business validation logic
}
}Location: application/
Components:
-
Ports (Interfaces)
ports/in/- Input ports (what the application does)ports/out/- Output ports (what the application needs)
-
Handlers (
handler/)- Receive requests from controllers
- Map DTOs to commands
- Call use case services
-
Services (
service/)- Implement use cases (input ports)
- Orchestrate domain services
- Use output ports for I/O
Flow:
Controller → Handler → Service (Use Case) → Domain Service
↓
Output Ports → Adapters
// Input port (use case interface)
public interface CreatePaymentUseCase {
Mono<Payment> createPayment(CreatePaymentCommand command);
}
// Output port (what we need from external systems)
public interface SavePaymentPort {
Mono<Payment> save(Payment payment);
}
// Service implements use case
@Service
public class CreatePaymentService implements CreatePaymentUseCase {
private final PaymentDomainService domainService;
private final SavePaymentPort savePaymentPort;
public Mono<Payment> createPayment(CreatePaymentCommand command) {
return buildPayment(command)
.flatMap(domainService::validate)
.flatMap(savePaymentPort::save);
}
}Location: adapter/
Inbound Adapters (adapter/in/)
- Web Controllers - HTTP REST endpoints
- Kafka Consumers - Event-driven triggers
- Scheduled Jobs - Time-based triggers
Outbound Adapters (adapter/out/)
- Database Repositories - R2DBC persistence
- REST Clients - HTTP calls to other services
- Kafka Producers - Event publishing
- Redis Cache - Caching layer
✅ Correct Structure (Fixed):
adapter/in/web/
├── dto/ ✅ HTTP DTOs belong here
├── mapper/ ✅ HTTP mappers belong here
└── controller/
adapter/out/db/
├── entity/ ✅ DB entities organized
├── mapper/ ✅ DB mappers organized
└── repository/
adapter/out/restclient/
├── dto/ ✅ External service DTOs
├── mapper/ ✅ External service mappers
└── client/
Location: infrastructure/
New Organized Structure:
infrastructure/
├─ config/ # Core application configuration
│ ├─ ApplicationConfig.java
│ ├─ ObjectMapperConfig.java
│ ├─ ValidationConfig.java
│ ├─ ReactorContextConfig.java
│ └─ ProblemDetailsConfig.java
├─ persistence/ # Database & transaction configuration
│ ├─ R2dbcConfig.java
│ ├─ TransactionConfig.java
│ ├─ MigrationConfig.java
│ └─ IdGeneratorConfig.java
├─ messaging/ # Kafka & event streaming
│ ├─ KafkaConfig.java
│ ├─ KafkaTopicConfig.java
│ └─ SerdeConfig.java
├─ cache/ # Redis & caching policies
│ ├─ RedisConfig.java
│ └─ CachePolicy.java
├─ http/ # WebClient & HTTP configuration
│ ├─ WebClientConfig.java
│ ├─ HttpClientObservability.java
│ └─ CorsAndRateLimitConfig.java
├─ security/ # Security & authentication
│ ├─ SecurityConfig.java
│ ├─ JwtConfig.java
│ └─ DataMaskingConfig.java
├─ observability/ # Monitoring & tracing
│ ├─ MetricsConfig.java
│ ├─ TracingConfig.java
│ └─ LoggingConfig.java
├─ resilience/ # Fault tolerance patterns
│ ├─ ResilienceConfig.java
│ └─ BackoffPolicy.java
├─ errorhandling/ # Global error handling
│ ├─ GlobalErrorAttributes.java
│ └─ GlobalExceptionHandler.java
└─ profiles/ # Environment configurations
├─ application.yml
├─ application-dev.yml
├─ application-staging.yml
└─ application-prod.yml
Contains:
- Config - Core application beans and configuration
- Persistence - Database, transactions, migrations, ID generation
- Messaging - Kafka configuration, topics, serialization
- Cache - Redis configuration and caching policies
- HTTP - WebClient, observability, CORS, rate limiting
- Security - Authentication, authorization, data masking
- Observability - Metrics, tracing, logging configuration
- Resilience - Circuit breakers, retry policies, bulkheads
- Error Handling - Global exception handling and error responses
- Profiles - Environment-specific configurations
Does NOT contain:
- ❌ Business DTOs
- ❌ Mappers (MapStruct interfaces)
- ❌ Entities
// ✅ Good: Infrastructure config
@Configuration
public class WebClientConfig {
@Bean
public WebClient accountServiceClient() {
return WebClient.builder()
.baseUrl("http://account-service")
.build();
}
}HTTP POST /api/v1/payments
1. PaymentController (adapter/in/web)
- Receives HTTP request
- Validates request body
↓
2. CreatePaymentRequestHandler (application/handler)
- Maps DTO to Command
- Calls use case
↓
3. CreatePaymentService (application/service)
- Implements CreatePaymentUseCase
- Orchestrates business flow
↓
4. PaymentDomainService (domain/service)
- Validates business rules
- Calculates fees, etc.
↓
5. SavePaymentPort → PaymentPersistenceAdapter (adapter/out/db)
- Saves to database via R2DBC
↓
6. PublishEventPort → PaymentEventProducer (adapter/out/kafka)
- Publishes event to Kafka
↓
7. Response flows back: Entity → Domain → DTO → HTTP
- Keep domain layer pure (no framework dependencies)
- Define clear port interfaces
- Use commands for input, events for output
- Map at adapter boundaries (Entity ↔ Domain ↔ DTO)
- Test business logic in isolation
- Don't expose domain entities directly via API
- Don't let domain depend on frameworks
- Don't put business logic in controllers
- Don't bypass ports (direct adapter access)
- Don't use DTOs in domain/application layers
// ❌ Bad: Nested flatMap
return a.flatMap(x ->
b(x).flatMap(y ->
c(x, y).flatMap(z ->
d(x, y, z)
)
)
);
// ✅ Good: Use zipWhen, delayUntil, then
return a
.zipWhen(this::b)
.flatMap(tuple -> c(tuple.getT1(), tuple.getT2()))
.delayUntil(this::d);return createPayment(command)
.doOnSuccess(p -> log.info("Payment created: {}", p.getId()))
.doOnError(e -> log.error("Failed: {}", e.getMessage()))
.onErrorMap(SQLException.class,
e -> new CustomException(CommonCode.DB_ERROR, e.getMessage()));// Unit test domain (no framework)
@Test
void shouldValidatePayment() {
PaymentDomainService service = new PaymentDomainService();
Payment payment = Payment.builder().amount(BigDecimal.ZERO).build();
assertThat(service.validatePayment(payment).isValid()).isFalse();
}
// Integration test with mocked ports
@Test
void shouldCreatePayment() {
SavePaymentPort mockPort = mock(SavePaymentPort.class);
CreatePaymentService service = new CreatePaymentService(mockPort, ...);
// Test business logic without database
}- Swap PostgreSQL for MongoDB: Only change adapter
- Replace REST with gRPC: Add new inbound adapter
- Add caching: Create cache adapter implementing ports
- Clear separation of concerns
- Business logic isolated from framework
- Easy to understand and modify
Hexagonal Architecture = Ports and Adapters
- Ports = Interfaces (what we need/provide)
- Adapters = Implementations (how we do it)
- Domain = Pure business logic (no dependencies)
- Application = Use cases (orchestration)
This separation allows you to:
- Test business logic without infrastructure
- Swap implementations easily
- Keep code maintainable and clean
- Follow SOLID principles naturally