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

20 KiB

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

<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:

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

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

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

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

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