1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 23:13:42 +01:00
effigenix/bin/.claude/skills/ddd-model/languages/java/structure.md
2026-02-18 23:25:12 +01:00

685 lines
20 KiB
Markdown

# Project Structure for Java DDD
This guide covers organizing a Java project following Clean Architecture and Domain-Driven Design principles.
## Maven Structure
Standard Maven project organization:
```
project-root/
├── pom.xml
├── src/
│ ├── main/
│ │ └── java/
│ │ └── com/example/
│ │ ├── domain/ # Domain layer
│ │ ├── application/ # Application layer
│ │ ├── infrastructure/ # Infrastructure layer
│ │ └── shared/ # Shared utilities
│ └── test/
│ └── java/
│ └── com/example/
│ ├── domain/
│ ├── application/
│ ├── infrastructure/
│ └── shared/
├── README.md
└── .gitignore
```
### Maven Dependencies Structure
```xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>banking-system</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Java 21 features (no additional dependencies needed) -->
<!-- Testing -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- Logging (infrastructure layer only) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<!-- Database (infrastructure only) -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
</plugins>
</build>
</project>
```
## Gradle Structure
Gradle-based alternative:
```gradle
plugins {
id 'java'
}
java {
sourceCompatibility = '21'
targetCompatibility = '21'
}
repositories {
mavenCentral()
}
dependencies {
// Testing
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
// Logging
implementation 'org.slf4j:slf4j-api:2.0.7'
runtimeOnly 'org.slf4j:slf4j-simple:2.0.7'
// Database
implementation 'org.postgresql:postgresql:42.6.0'
}
test {
useJUnitPlatform()
}
```
## Complete Directory Structure
### Domain Layer
```
src/main/java/com/example/domain/
├── account/ # Bounded context: Account
│ ├── Account.java # Aggregate root
│ ├── AccountId.java # Value object (ID)
│ ├── AccountStatus.java # Value object (enum-like)
│ ├── Money.java # Value object (shared across contexts)
│ ├── AccountError.java # Sealed interface for errors
│ ├── AccountRepository.java # Repository interface (domain contract)
│ └── DomainEventPublisher.java # Event publishing interface (optional)
├── transfer/ # Bounded context: Transfer
│ ├── Transfer.java # Aggregate root
│ ├── TransferId.java # Value object
│ ├── TransferStatus.java # Status enum
│ ├── TransferError.java # Errors
│ ├── TransferRepository.java # Repository interface
│ └── TransferService.java # Domain service (if needed)
├── shared/
│ ├── result/
│ │ ├── Result.java # Result<E, T> interface
│ │ ├── Success.java # Success case
│ │ └── Failure.java # Failure case
│ └── DomainEvent.java # Base domain event
```
### Application Layer
```
src/main/java/com/example/application/
├── account/
│ ├── OpenAccountUseCase.java # One use case per file
│ ├── DepositMoneyUseCase.java
│ ├── WithdrawMoneyUseCase.java
│ ├── GetAccountBalanceUseCase.java
│ ├── dto/
│ │ ├── OpenAccountRequest.java
│ │ ├── OpenAccountResponse.java
│ │ ├── DepositRequest.java
│ │ └── DepositResponse.java
│ └── AccountApplicationError.java # App-specific errors
├── transfer/
│ ├── TransferMoneyUseCase.java
│ ├── GetTransferStatusUseCase.java
│ ├── dto/
│ │ ├── TransferRequest.java
│ │ └── TransferResponse.java
│ └── TransferApplicationError.java
├── shared/
│ ├── UseCase.java # Interface/base class for use cases
│ └── UnitOfWork.java # Transaction management interface
```
### Infrastructure Layer
```
src/main/java/com/example/infrastructure/
├── persistence/
│ ├── account/
│ │ ├── JdbcAccountRepository.java # Implements AccountRepository
│ │ ├── AccountRowMapper.java # Database row mapping
│ │ └── AccountQueries.java # SQL constants
│ ├── transfer/
│ │ ├── JdbcTransferRepository.java
│ │ └── TransferRowMapper.java
│ ├── connection/
│ │ ├── ConnectionPool.java
│ │ └── DataSourceFactory.java
│ └── transaction/
│ └── JdbcUnitOfWork.java # Transaction coordinator
├── http/
│ ├── handler/
│ │ ├── account/
│ │ │ ├── OpenAccountHandler.java
│ │ │ ├── WithdrawHandler.java
│ │ │ └── GetBalanceHandler.java
│ │ └── transfer/
│ │ ├── TransferHandler.java
│ │ └── GetTransferStatusHandler.java
│ ├── router/
│ │ └── ApiRouter.java # Route definition
│ ├── response/
│ │ ├── SuccessResponse.java
│ │ └── ErrorResponse.java
│ └── middleware/
│ ├── ErrorHandlingMiddleware.java
│ └── LoggingMiddleware.java
├── event/
│ ├── DomainEventPublisherImpl.java # Publishes domain events
│ ├── event-handlers/
│ │ ├── AccountCreatedEventHandler.java
│ │ └── TransferCompletedEventHandler.java
│ └── EventDispatcher.java
├── config/
│ └── AppConfiguration.java # Dependency injection setup
└── persistence/
└── migrations/
├── V001__CreateAccountsTable.sql
└── V002__CreateTransfersTable.sql
```
### Test Structure
```
src/test/java/com/example/
├── domain/
│ ├── account/
│ │ ├── AccountTest.java # Unit tests for Account
│ │ ├── MoneyTest.java
│ │ └── AccountRepositoryTest.java # Contract tests
│ └── transfer/
│ ├── TransferTest.java
│ └── TransferRepositoryTest.java
├── application/
│ ├── account/
│ │ ├── OpenAccountUseCaseTest.java
│ │ ├── WithdrawMoneyUseCaseTest.java
│ │ └── fixtures/
│ │ ├── AccountFixture.java # Test data builders
│ │ └── MoneyFixture.java
│ └── transfer/
│ └── TransferMoneyUseCaseTest.java
├── infrastructure/
│ ├── persistence/
│ │ ├── JdbcAccountRepositoryTest.java # Integration tests
│ │ └── JdbcTransferRepositoryTest.java
│ └── http/
│ └── OpenAccountHandlerTest.java
└── acceptance/
└── OpenAccountAcceptanceTest.java # End-to-end tests
```
## Three Organizational Approaches
### Approach 1: BC-First (Recommended for Most Projects)
Organize around Bounded Contexts:
```
src/main/java/com/example/
├── account/ # BC 1
│ ├── domain/
│ │ ├── Account.java
│ │ ├── AccountError.java
│ │ └── AccountRepository.java
│ ├── application/
│ │ ├── OpenAccountUseCase.java
│ │ └── dto/
│ └── infrastructure/
│ ├── persistence/
│ │ └── JdbcAccountRepository.java
│ └── http/
│ └── OpenAccountHandler.java
├── transfer/ # BC 2
│ ├── domain/
│ ├── application/
│ └── infrastructure/
└── shared/ # Shared across BCs
├── result/
│ └── Result.java
└── events/
```
**Pros:**
- Clear BC boundaries
- Easy to navigate between layers within a context
- Natural place for context-specific configuration
- Facilitates team ownership per BC
**Cons:**
- Duplication across contexts
- More code organization overhead
### Approach 2: Tech-First (Better for Microservices)
Organize by technical layer:
```
src/main/java/com/example/
├── domain/
│ ├── account/
│ │ ├── Account.java
│ │ ├── AccountRepository.java
│ │ └── AccountError.java
│ └── transfer/
├── application/
│ ├── account/
│ │ ├── OpenAccountUseCase.java
│ │ └── dto/
│ └── transfer/
├── infrastructure/
│ ├── persistence/
│ │ ├── account/
│ │ │ └── JdbcAccountRepository.java
│ │ └── transfer/
│ ├── http/
│ │ ├── account/
│ │ └── transfer/
│ └── config/
└── shared/
```
**Pros:**
- Clear layer separation
- Easy to review layer architecture
- Good for enforcing dependency rules
**Cons:**
- Scattered BC concepts across files
- Harder to find all code for one feature
### Approach 3: Hybrid (Best for Large Projects)
Combine both approaches strategically:
```
src/main/java/com/example/
├── domain/ # All domain objects, shared across project
│ ├── account/
│ ├── transfer/
│ └── shared/
├── application/ # All application services
│ ├── account/
│ └── transfer/
├── infrastructure/ # Infrastructure organized by BC
│ ├── account/
│ │ ├── persistence/
│ │ └── http/
│ ├── transfer/
│ ├── config/
│ └── shared/
```
**Pros:**
- Emphasizes domain independence
- Clear infrastructure layer separation
- Good for large teams
**Cons:**
- Two different organizational styles
- Requires discipline to maintain
## One Use Case Per File
### Recommended Structure
```
src/main/java/com/example/application/account/
├── OpenAccountUseCase.java
├── DepositMoneyUseCase.java
├── WithdrawMoneyUseCase.java
├── GetAccountBalanceUseCase.java
├── CloseAccountUseCase.java
└── dto/
├── OpenAccountRequest.java
├── OpenAccountResponse.java
├── DepositRequest.java
└── DepositResponse.java
```
### Use Case File Template
```java
package com.example.application.account;
import com.example.domain.account.*;
import com.example.shared.result.Result;
import static com.example.shared.result.Result.success;
import static com.example.shared.result.Result.failure;
/**
* OpenAccountUseCase - one file, one use case.
*
* Coordinates the opening of a new account:
* 1. Create Account aggregate
* 2. Persist via repository
* 3. Publish domain events
*/
public class OpenAccountUseCase {
private final AccountRepository accountRepository;
private final AccountIdGenerator idGenerator;
private final UnitOfWork unitOfWork;
public OpenAccountUseCase(
AccountRepository accountRepository,
AccountIdGenerator idGenerator,
UnitOfWork unitOfWork
) {
this.accountRepository = accountRepository;
this.idGenerator = idGenerator;
this.unitOfWork = unitOfWork;
}
/**
* Execute the use case.
*
* @param request containing account opening parameters
* @return success with account ID, or failure with reason
*/
public Result<OpenAccountError, OpenAccountResponse> execute(
OpenAccountRequest request
) {
try {
// Phase 1: Create aggregate
AccountId accountId = idGenerator.generate();
Result<AccountError, Account> accountResult = Account.create(
accountId,
request.initialBalance(),
request.accountHolder()
);
if (accountResult instanceof Failure f) {
return failure(mapError(f.error()));
}
Account account = ((Success<AccountError, Account>) accountResult).value();
// Phase 2: Persist
return unitOfWork.withTransaction(() -> {
accountRepository.save(account);
// Phase 3: Publish events
account.publishedEvents().forEach(event ->
eventPublisher.publish(event)
);
return success(new OpenAccountResponse(
account.id().value(),
account.balance()
));
});
} catch (Exception e) {
logger.error("Unexpected error opening account", e);
return failure(new OpenAccountError.RepositoryError("Failed to save account"));
}
}
private OpenAccountError mapError(AccountError error) {
return switch (error) {
case InvalidAmountError e ->
new OpenAccountError.InvalidInitialBalance(e.message());
case InvalidAccountHolderError e ->
new OpenAccountError.InvalidHolder(e.message());
default ->
new OpenAccountError.UnexpectedError(error.message());
};
}
}
/**
* OpenAccountRequest - input DTO.
*/
public record OpenAccountRequest(
Money initialBalance,
String accountHolder
) {
public OpenAccountRequest {
if (initialBalance == null) {
throw new IllegalArgumentException("Initial balance required");
}
if (accountHolder == null || accountHolder.isBlank()) {
throw new IllegalArgumentException("Account holder name required");
}
}
}
/**
* OpenAccountResponse - output DTO.
*/
public record OpenAccountResponse(
String accountId,
Money balance
) {}
/**
* OpenAccountError - use case specific errors.
*/
public sealed interface OpenAccountError permits
InvalidInitialBalance,
InvalidHolder,
RepositoryError,
UnexpectedError {
String message();
}
public record InvalidInitialBalance(String reason) implements OpenAccountError {
@Override
public String message() {
return "Invalid initial balance: " + reason;
}
}
// ... other error implementations
```
## Package Naming
```
com.example # Root
├── domain # Domain layer
│ ├── account # BC 1 domain
│ │ └── Account.java
│ └── transfer # BC 2 domain
│ └── Transfer.java
├── application # Application layer
│ ├── account # BC 1 use cases
│ │ └── OpenAccountUseCase.java
│ └── transfer # BC 2 use cases
│ └── TransferMoneyUseCase.java
├── infrastructure # Infrastructure layer
│ ├── persistence # Persistence adapters
│ │ ├── account # BC 1 persistence
│ │ └── transfer # BC 2 persistence
│ ├── http # HTTP adapters
│ │ ├── account # BC 1 handlers
│ │ └── transfer # BC 2 handlers
│ └── config # Configuration
│ └── AppConfiguration.java
└── shared # Shared across layers
├── result # Result<E, T>
├── events # Domain events
└── exceptions # Shared exceptions (use sparingly)
```
## Example: Account BC Structure
Complete example of one bounded context:
```
com/example/account/
├── domain/
│ ├── Account.java # Aggregate root
│ ├── AccountId.java # ID value object
│ ├── AccountStatus.java # Status value object
│ ├── AccountError.java # Sealed error interface
│ ├── AccountRepository.java # Repository interface
│ ├── DomainEvents.java # Domain events (AccountCreated, etc.)
│ └── AccountIdGenerator.java # Generator interface
├── application/
│ ├── OpenAccountUseCase.java
│ ├── DepositMoneyUseCase.java
│ ├── WithdrawMoneyUseCase.java
│ ├── GetAccountBalanceUseCase.java
│ ├── ApplicationError.java # App-level errors
│ ├── dto/
│ │ ├── OpenAccountRequest.java
│ │ ├── OpenAccountResponse.java
│ │ ├── DepositRequest.java
│ │ └── DepositResponse.java
│ └── fixtures/ (test directory)
│ └── AccountFixture.java
└── infrastructure/
├── persistence/
│ ├── JdbcAccountRepository.java
│ ├── AccountRowMapper.java
│ └── AccountQueries.java
├── http/
│ ├── OpenAccountHandler.java
│ ├── DepositHandler.java
│ ├── WithdrawHandler.java
│ └── GetBalanceHandler.java
└── events/
└── AccountEventHandlers.java
```
## Configuration Example
```java
package com.example.infrastructure.config;
public class AppConfiguration {
private final DataSource dataSource;
private final DomainEventPublisher eventPublisher;
public AppConfiguration() {
this.dataSource = createDataSource();
this.eventPublisher = createEventPublisher();
}
// Account BC dependencies
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
public AccountIdGenerator accountIdGenerator() {
return new UuidAccountIdGenerator();
}
public OpenAccountUseCase openAccountUseCase() {
return new OpenAccountUseCase(
accountRepository(),
accountIdGenerator(),
unitOfWork()
);
}
// Transfer BC dependencies
public TransferRepository transferRepository() {
return new JdbcTransferRepository(dataSource);
}
public TransferMoneyUseCase transferMoneyUseCase() {
return new TransferMoneyUseCase(
accountRepository(),
transferRepository(),
unitOfWork()
);
}
// Shared infrastructure
public UnitOfWork unitOfWork() {
return new JdbcUnitOfWork(dataSource);
}
public DomainEventPublisher eventPublisher() {
return eventPublisher;
}
private DataSource createDataSource() {
// Database pool configuration
return new HikariDataSource();
}
private DomainEventPublisher createEventPublisher() {
return new SimpleEventPublisher();
}
}
```
## Best Practices
1. **Organize by BC first** when starting a project
2. **One use case per file** in application layer
3. **Keep test directory structure** parallel to main
4. **Place DTOs near their use cases** (not in separate folder)
5. **Shared code in `shared` package** (Result, base classes)
6. **Database migrations** in dedicated folder
7. **Configuration at root** of infrastructure layer
8. **HTTP handlers** group by BC
9. **Repository implementations** group by BC
10. **No circular package dependencies** - enforce with checkstyle