Skip to content

Latest commit

 

History

History
526 lines (433 loc) · 14.4 KB

File metadata and controls

526 lines (433 loc) · 14.4 KB

Implementation Guide

Step-by-Step Process

Phase 1: Define Domain (Pure Business Logic)

1.1 Create Domain Entities

// 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();
  }
}

1.2 Create Domain Services

// 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);
  }
}

Phase 2: Define Application Ports

2.1 Input Ports (Use Cases)

// 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;
}

2.2 Output Ports (Dependencies)

// 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);
}

Phase 3: Implement Application Services

// 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);
  }
}

Phase 4: Create Request Handlers

// 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();
  }
}

Phase 5: Implement Adapters

5.1 Inbound Adapter - REST Controller

// 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);
  }
}

5.2 Outbound Adapter - Database Repository

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);
  }
}

Phase 6: Create Infrastructure

6.1 MapStruct Mappers

// 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);
}

6.2 Spring Configuration

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);
  }
}

6.3 Database Migration

-- 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);

Phase 7: Testing

7.1 Domain Tests (Pure Unit Tests)

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();
  }
}

7.2 Service Tests (Mock Ports)

@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();
  }
}

Reactive Patterns

Sequential Operations

return step1()
  .flatMap(this::step2)
  .flatMap(this::step3);

Parallel Operations

return step1()
  .zipWhen(this::step2)
  .flatMap(tuple -> step3(tuple.getT1(), tuple.getT2()));

Side Effects

return mainFlow()
  .delayUntil(result -> sideEffect(result))
  .doOnSuccess(result -> log.info("Done: {}", result));

Common Patterns

Adding External Service

// 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);
  }
}

Adding Caching

@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);
  }
}

New Infrastructure Organization

Configuration Categories

1. Core Configuration (infrastructure/config/)

  • ApplicationConfig.java - Domain service beans and core wiring
  • ObjectMapperConfig.java - JSON serialization configuration
  • ValidationConfig.java - Bean validation setup
  • ReactorContextConfig.java - Reactive context and hooks
  • ProblemDetailsConfig.java - RFC 7807 error response format

2. Persistence Configuration (infrastructure/persistence/)

  • R2dbcConfig.java - Reactive database configuration
  • TransactionConfig.java - Transaction management
  • MigrationConfig.java - Flyway database migrations
  • IdGeneratorConfig.java - ID generation strategies (UUID, ULID)

3. Messaging Configuration (infrastructure/messaging/)

  • KafkaConfig.java - Reactive Kafka producer/consumer
  • KafkaTopicConfig.java - Topic creation and management
  • SerdeConfig.java - Message serialization/deserialization

4. Cache Configuration (infrastructure/cache/)

  • RedisConfig.java - Reactive Redis template
  • CachePolicy.java - TTL policies and key strategies

5. HTTP Configuration (infrastructure/http/)

  • WebClientConfig.java - External service clients
  • HttpClientObservability.java - Request/response logging and metrics
  • CorsAndRateLimitConfig.java - CORS and rate limiting

6. Security Configuration (infrastructure/security/)

  • SecurityConfig.java - WebFlux security setup
  • JwtConfig.java - JWT authentication and authorization
  • DataMaskingConfig.java - PII protection and log masking

7. Observability Configuration (infrastructure/observability/)

  • MetricsConfig.java - Micrometer metrics and business metrics
  • TracingConfig.java - Distributed tracing with OpenTelemetry
  • LoggingConfig.java - Reactive logging and correlation IDs

8. Resilience Configuration (infrastructure/resilience/)

  • ResilienceConfig.java - Circuit breakers, retry, bulkhead, time limiter
  • BackoffPolicy.java - Various backoff strategies for retries

9. Error Handling Configuration (infrastructure/errorhandling/)

  • GlobalErrorAttributes.java - Standardized error attributes
  • GlobalExceptionHandler.java - Global exception mapping to HTTP responses

10. Profile Configuration (infrastructure/profiles/)

  • application.yml - Base configuration
  • application-dev.yml - Development environment
  • application-staging.yml - Staging environment
  • application-prod.yml - Production environment

Benefits of New Structure

  1. Separation of Concerns - Each configuration category has a specific responsibility
  2. Easier Maintenance - Related configurations are grouped together
  3. Better Testability - Individual configuration classes can be tested in isolation
  4. Environment Management - Clear separation of environment-specific settings
  5. Production Ready - Comprehensive configuration for observability, security, and resilience

Checklist

  • 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

Tips

  1. Start Simple: Begin with one use case
  2. Keep Domain Pure: No framework dependencies
  3. Test Independently: Mock ports for fast tests
  4. Use Reactive Patterns: Avoid deep nesting
  5. Follow Structure: DTOs in adapters, config in infrastructure