Now that weβve separated concerns and cleaned up the code, itβs time for the next step:
introducing Clean Architecture layers and applying DDD concepts with proper domain modeling.
Letβs make the design clearer, scalable, and truly domain-focused. π
π§ Step 1: Laying the Groundwork for Clean Architecture
Before we can apply Clean Architecture and DDD properly, we need to rethink our folder structure.
Right now, everything is grouped by technical role:
com.example.spaghetti
βββ controller
β βββ ThingController.java
β βββ ItemController.java
βββ service
β βββ ThingService.java
β βββ ItemService.java
βββ repository
β βββ ThingRepository.java
β βββ ItemRepository.java
βββ model
β βββ Thing.java
β βββ Item.java
βββ config
β βββ MainConfig.java
βββ util
β βββ Utils.java
Thatβs a good start β but Clean Architecture is not just about naming layers, itβs about isolating responsibilities and controlling dependencies between them.
β
1. Define the main layers
Hereβs the structure weβre aiming for:
com.example.spaghetti
βββ domain β business rules and core entities (pure Java)
βββ application β use cases (calls domain logic)
βββ infrastructure β technical details (config, persistence, utilities)
βββ adapter β I/O layers (controllers, REST APIs, DTOs)
π‘ Rule of thumb:
- Domain is the core.
- Application uses the domain.
- Infrastructure supports them.
- Adapter is how the outside world talks to the app.
β
2. Move your classes to the right packages
Letβs organize your existing code into those layers:
πΉ domain
Core entities and domain contracts
com.example.spaghetti.domain.model
βββ Thing.java
βββ Item.java
com.example.spaghetti.domain.repository
βββ ThingRepository.java
βββ ItemRepository.java
πΉ application
Business coordination logic
com.example.spaghetti.application.service
βββ ThingService.java
βββ ItemService.java
πΉ infrastructure
Low-level details like config and utility classes
com.example.spaghetti.infrastructure.config
βββ MainConfig.java
com.example.spaghetti.infrastructure.util
βββ Utils.java
πΉ adapter
Web interface and DTOs
com.example.spaghetti.adapter.controller
βββ ThingController.java
βββ ItemController.java
com.example.spaghetti.adapter.dto
βββ (we'll add DTOs in the next steps)
This new structure aligns your code with Clean Architecture principles, making it easier to scale, test, and evolve over time.
π§ Step 2: Introducing DTOs, Mappers, and Validation for Clear Separation
After reorganizing our project structure and separating concerns, the next essential step was to make the boundary between our API and domain logic explicit β by introducing DTOs (Data Transfer Objects), validation, and mappers.
π Organizing DTOs and Mappers
We created dedicated packages to keep the code clean and scalable:
com.example.spaghetti.adapter.dto.request β Input DTOs (ThingRequest, ItemRequest)
com.example.spaghetti.adapter.dto.response β Output DTOs (ThingResponse, ItemResponse)
com.example.spaghetti.adapter.dto.mapper β Mappers to convert between DTOs and domain entities
This separation keeps the API contract clear, prevents domain leakage, and isolates transformations, following Clean Architectureβs adapter layer principle.
π Why DTOs?
DTOs define exactly what data enters and leaves our system. To ensure data integrity, we added validation annotations from Jakarta Validation API, such as NotBlank and NotNull, on the request DTO fields.
This guarantees that only valid and well-formed data reaches the business logic layer.
By doing this, our domain entities remain clean and focused solely on business rules β an important DDD principle to avoid coupling domain models to external layers.
π οΈ Using MapStruct for Mapping
To avoid boilerplate code when converting between DTOs and entities, we introduced MapStruct, a code generator that creates type-safe mappers automatically:
@Mapper(componentModel = "spring")
public interface ThingMapper {
Thing toEntity(ThingRequest request);
ThingResponse toResponse(Thing thing);
List<ThingResponse> toResponse(List<Thing> things);
}
@Mapper(componentModel = "spring")
public interface ItemMapper {
Item toEntity(ItemRequest request);
ItemResponse toResponse(Item item);
List<ItemResponse> toResponseList(List<Item> things);
}
Mappers live in the adapter layer and keep our domain pure from external concerns.
βοΈ Controller Adjustments
Controllers were updated to:
- Accept validated request DTOs (annotated with Jakarta Validation constraints)
- Use mappers to convert requests into domain entities
- Call the service layer with domain entities
- Convert service results back into response DTOs for the client
- This keeps the controllerβs responsibility to a simple orchestrator β no business logic inside, fully aligned with Clean Architecture.
β What this achieved
- Clear separation of API and domain models
- Early validation of input data via Jakarta Validation annotations
- Reduced boilerplate with MapStruct generated code
- Improved maintainability and scalability of the codebase
π§ Whatβs next?
With DTOs, validation, and mapping in place, our code is now clean, scalable, and aligned with Clean Architecture and DDD principles.
Next up, weβll focus on improving reliability by implementing global error handling and writing unit tests.
Top comments (0)