// domain/model/YourEntity.java
@Data @Builder @NoArgsConstructor @AllArgsConstructor
public class YourEntity {
private String id;
private String userId;
private BigDecimal amount;
private Status status;
// Business rules as methods
public boolean isValid() {
return amount != null && amount.compareTo(BigDecimal.ZERO) > 0;
}
public void complete() {
if (this.status == Status.COMPLETED) {
throw new IllegalStateException("Already completed");
}
this.status = Status.COMPLETED;
this.completedAt = Instant.now();
}
}// domain/service/YourDomainService.java
public class YourDomainService {
public ValidationResult validate(YourEntity entity) {
if (entity == null) {
return ValidationResult.invalid("Entity cannot be null");
}
// ... more business rules
return ValidationResult.valid();
}
public BigDecimal calculateFee(YourEntity entity) {
// Complex fee calculation logic
return baseFee.add(variableFee);
}
}// application/ports/in/YourUseCase.java
public interface YourUseCase {
Mono<YourEntity> execute(YourCommand command);
}
// Command object (immutable)
@Value @Builder
public class YourCommand {
String userId;
BigDecimal amount;
String currency;
String idempotencyKey;
}// application/ports/out/SaveYourEntityPort.java
public interface SaveYourEntityPort {
Mono<YourEntity> save(YourEntity entity);
}
// application/ports/out/LoadYourEntityPort.java
public interface LoadYourEntityPort {
Mono<YourEntity> findById(String id);
Flux<YourEntity> findByUserId(String userId);
}// application/service/YourUseCaseService.java
@Service @RequiredArgsConstructor @Slf4j
public class YourUseCaseService implements YourUseCase {
private final YourDomainService domainService;
private final SaveYourEntityPort savePort;
private final LoadYourEntityPort loadPort;
@Override
public Mono<YourEntity> execute(YourCommand command) {
return checkIdempotency(command.getIdempotencyKey())
.switchIfEmpty(processNew(command));
}
private Mono<YourEntity> processNew(YourCommand command) {
return Mono.just(buildEntity(command))
.flatMap(this::validateEntity)
.flatMap(savePort::save)
.doOnSuccess(e -> log.info("Entity created: {}", e.getId()));
}
private Mono<YourEntity> validateEntity(YourEntity entity) {
ValidationResult result = domainService.validate(entity);
if (!result.isValid()) {
return Mono.error(
new CustomException(CommonCode.INVALID_REQUEST, result.errorMessage())
);
}
return Mono.just(entity);
}
}// application/handler/YourRequestHandler.java
@Component @RequiredArgsConstructor @Slf4j
public class YourRequestHandler extends RequestHandler<YourRequest, YourResponse> {
private final YourUseCase useCase;
private final YourMapper mapper;
@Override
public Mono<YourResponse> handle(YourRequest request) {
log.info("Handling request for user: {}", request.getUserId());
return Mono.just(request)
.map(this::toCommand)
.flatMap(useCase::execute)
.map(mapper::toResponse);
}
private YourCommand toCommand(YourRequest request) {
return YourCommand.builder()
.userId(request.getUserId())
.amount(request.getAmount())
.build();
}
}// adapter/in/web/YourController.java
@RestController
@RequestMapping("/api/v1/your-resource")
@RequiredArgsConstructor @Slf4j
public class YourController {
private final YourRequestHandler handler;
@PostMapping
public Mono<ApiResponse<YourResponse>> create(
@Valid @RequestBody YourRequest request) {
return handler.handle(request)
.map(ApiResponse::success);
}
@GetMapping("/{id}")
public Mono<ApiResponse<YourResponse>> get(@PathVariable String id) {
GetYourEntityRequest request = new GetYourEntityRequest(id);
return getHandler.handle(request)
.map(ApiResponse::success);
}
}Step 1: Create Entity
// adapter/out/db/entity/YourEntityDb.java
@Data @Builder @NoArgsConstructor @AllArgsConstructor
@Table("your_table")
public class YourEntityDb {
@Id
private String id;
@Column("user_id")
private String userId;
@Column("amount")
private BigDecimal amount;
@Column("status")
private String status;
@Column("created_at")
private Instant createdAt;
}Step 2: Create Repository
// adapter/out/db/YourR2dbcRepository.java
@Repository
public interface YourR2dbcRepository extends R2dbcRepository<YourEntityDb, String> {
Flux<YourEntityDb> findByUserId(String userId);
Mono<YourEntityDb> findByReferenceId(String referenceId);
}Step 3: Create Persistence Adapter
// adapter/out/db/YourPersistenceAdapter.java
@Component @RequiredArgsConstructor @Slf4j
public class YourPersistenceAdapter implements SaveYourEntityPort, LoadYourEntityPort {
private final YourR2dbcRepository repository;
private final YourEntityMapper mapper;
@Override
public Mono<YourEntity> save(YourEntity entity) {
return Mono.just(entity)
.map(mapper::toDb)
.flatMap(repository::save)
.map(mapper::toDomain);
}
@Override
public Mono<YourEntity> findById(String id) {
return repository.findById(id)
.map(mapper::toDomain);
}
}// adapter/in/web/mapper/YourWebMapper.java
@Mapper(componentModel = "spring")
public interface YourWebMapper {
YourResponse toResponse(YourEntity entity);
YourEntity toDomain(YourRequest request);
}
// adapter/out/db/mapper/YourEntityMapper.java
@Mapper(componentModel = "spring")
public interface YourEntityMapper {
YourEntityDb toDb(YourEntity entity);
YourEntity toDomain(YourEntityDb entityDb);
}New Infrastructure Structure:
infrastructure/
├─ config/ # Core application configuration
├─ persistence/ # Database & transaction configuration
├─ messaging/ # Kafka & event streaming
├─ cache/ # Redis & caching policies
├─ http/ # WebClient & HTTP configuration
├─ security/ # Security & authentication
├─ observability/ # Monitoring & tracing
├─ resilience/ # Fault tolerance patterns
├─ errorhandling/ # Global error handling
└─ profiles/ # Environment configurations
// infrastructure/config/ApplicationConfig.java
@Configuration
public class ApplicationConfig {
@Bean
public YourDomainService yourDomainService() {
return new YourDomainService();
}
}
// infrastructure/http/WebClientConfig.java
@Configuration
public class WebClientConfig {
@Bean
public WebClient externalServiceClient(
@Value("${services.external.base-url}") String baseUrl) {
return WebClient.builder()
.baseUrl(baseUrl)
.build();
}
}
// infrastructure/persistence/R2dbcConfig.java
@Configuration
@EnableR2dbcRepositories(basePackages = "com.example.payments.adapter.out.db")
@EnableR2dbcAuditing
public class R2dbcConfig {
@Bean
public ReactiveTransactionManager transactionManager(ConnectionFactory connectionFactory) {
return new R2dbcTransactionManager(connectionFactory);
}
}-- infrastructure/persistence/V1__Create_your_table.sql
CREATE TABLE your_table (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
amount DECIMAL(20, 8) NOT NULL,
currency VARCHAR(10) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_your_table_user_id ON your_table (user_id);
CREATE INDEX idx_your_table_status ON your_table (status);class YourDomainServiceTest {
private YourDomainService service = new YourDomainService();
@Test
void shouldValidateEntity() {
YourEntity entity = YourEntity.builder()
.amount(BigDecimal.valueOf(100))
.build();
ValidationResult result = service.validate(entity);
assertThat(result.isValid()).isTrue();
}
}@ExtendWith(MockitoExtension.class)
class YourUseCaseServiceTest {
@Mock private SaveYourEntityPort savePort;
@InjectMocks private YourUseCaseService service;
@Test
void shouldExecuteUseCase() {
YourCommand command = YourCommand.builder()
.userId("user123")
.amount(BigDecimal.valueOf(100))
.build();
when(savePort.save(any())).thenAnswer(i -> Mono.just(i.getArgument(0)));
StepVerifier.create(service.execute(command))
.assertNext(entity -> {
assertThat(entity.getUserId()).isEqualTo("user123");
})
.verifyComplete();
}
}return step1()
.flatMap(this::step2)
.flatMap(this::step3);return step1()
.zipWhen(this::step2)
.flatMap(tuple -> step3(tuple.getT1(), tuple.getT2()));return mainFlow()
.delayUntil(result -> sideEffect(result))
.doOnSuccess(result -> log.info("Done: {}", result));// Define port
public interface ExternalServicePort {
Mono<Result> callService(Request request);
}
// Implement adapter
@Component
public class ExternalServiceClient implements ExternalServicePort {
private final WebClient webClient;
public Mono<Result> callService(Request request) {
return webClient.post()
.uri("/api/endpoint")
.bodyValue(request)
.retrieve()
.bodyToMono(Result.class);
}
}@Component
public class YourCacheAdapter {
private final ReactiveRedisTemplate<String, String> redisTemplate;
public Mono<YourEntity> getCached(String id) {
return redisTemplate.opsForValue()
.get("entity:" + id)
.map(this::deserialize);
}
}ApplicationConfig.java- Domain service beans and core wiringObjectMapperConfig.java- JSON serialization configurationValidationConfig.java- Bean validation setupReactorContextConfig.java- Reactive context and hooksProblemDetailsConfig.java- RFC 7807 error response format
R2dbcConfig.java- Reactive database configurationTransactionConfig.java- Transaction managementMigrationConfig.java- Flyway database migrationsIdGeneratorConfig.java- ID generation strategies (UUID, ULID)
KafkaConfig.java- Reactive Kafka producer/consumerKafkaTopicConfig.java- Topic creation and managementSerdeConfig.java- Message serialization/deserialization
RedisConfig.java- Reactive Redis templateCachePolicy.java- TTL policies and key strategies
WebClientConfig.java- External service clientsHttpClientObservability.java- Request/response logging and metricsCorsAndRateLimitConfig.java- CORS and rate limiting
SecurityConfig.java- WebFlux security setupJwtConfig.java- JWT authentication and authorizationDataMaskingConfig.java- PII protection and log masking
MetricsConfig.java- Micrometer metrics and business metricsTracingConfig.java- Distributed tracing with OpenTelemetryLoggingConfig.java- Reactive logging and correlation IDs
ResilienceConfig.java- Circuit breakers, retry, bulkhead, time limiterBackoffPolicy.java- Various backoff strategies for retries
GlobalErrorAttributes.java- Standardized error attributesGlobalExceptionHandler.java- Global exception mapping to HTTP responses
application.yml- Base configurationapplication-dev.yml- Development environmentapplication-staging.yml- Staging environmentapplication-prod.yml- Production environment
- Separation of Concerns - Each configuration category has a specific responsibility
- Easier Maintenance - Related configurations are grouped together
- Better Testability - Individual configuration classes can be tested in isolation
- Environment Management - Clear separation of environment-specific settings
- Production Ready - Comprehensive configuration for observability, security, and resilience
- Domain entities created (pure POJOs)
- Domain services for complex logic
- Input ports defined (use cases)
- Output ports defined (dependencies)
- Application services implement use cases
- Request handlers created
- Inbound adapters (controllers, consumers)
- Outbound adapters (repositories, clients)
- MapStruct mappers configured
- Infrastructure configuration organized by category
- Environment-specific configurations created
- Database migrations created
- Security configuration implemented
- Observability and monitoring configured
- Resilience patterns implemented
- Error handling standardized
- Unit tests for domain
- Integration tests for services
- E2E tests for controllers
- Start Simple: Begin with one use case
- Keep Domain Pure: No framework dependencies
- Test Independently: Mock ports for fast tests
- Use Reactive Patterns: Avoid deep nesting
- Follow Structure: DTOs in adapters, config in infrastructure