DEV Community

Cover image for 🧠 From Chaos to Clean Code: My Java Refactor Journey - Part 3 of 6
Gabriela Goudromihos Puig
Gabriela Goudromihos Puig

Posted on

🧠 From Chaos to Clean Code: My Java Refactor Journey - Part 3 of 6

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ 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
Enter fullscreen mode Exit fullscreen mode
com.example.spaghetti.domain.repository
  └── ThingRepository.java
  └── ItemRepository.java
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή application
Business coordination logic

com.example.spaghetti.application.service
  └── ThingService.java
  └── ItemService.java
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή infrastructure
Low-level details like config and utility classes

com.example.spaghetti.infrastructure.config
  └── MainConfig.java
Enter fullscreen mode Exit fullscreen mode
com.example.spaghetti.infrastructure.util
  └── Utils.java
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή adapter
Web interface and DTOs

com.example.spaghetti.adapter.controller
  └── ThingController.java
  └── ItemController.java
Enter fullscreen mode Exit fullscreen mode
com.example.spaghetti.adapter.dto
  └── (we'll add DTOs in the next steps)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
@Mapper(componentModel = "spring")
public interface ItemMapper {
    Item toEntity(ItemRequest request);
    ItemResponse toResponse(Item item);
    List<ItemResponse> toResponseList(List<Item> things);
}
Enter fullscreen mode Exit fullscreen mode

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)