This content originally appeared on DEV Community and was authored by Gabriela Goudromihos Puig
Step 1: Separating Responsibilities β From Spaghetti to Structure
The first class I refactored was a typical all-in-one mess: controller, business logic, and in-memory storage were all crammed into the same place.
@RestController
@RequestMapping("/things")
public class ControllerService {
private List<String> list = new ArrayList<>();
// ...
}
@RestController
@RequestMapping("/items")
public class ModelRepoController {
private Map<Long, Item> storage = new HashMap<>();
// ...
}
This class did everything β which is exactly what we want to avoid in domain-oriented architecture.
The refactor
Split the logic into four separate components:
- ThingController/ItemController: exposes the HTTP API
- ThingService/ItemService: handles the business logic
- ThingRepository/ItemRepository: manages data access (in-memory for now)
- Thing/Item: represents the domain entity
The logic stayed the same, but now it’s clean, testable, and aligned with Clean Architecture principles.
Before: logic and storage were tightly coupled in the controller
After: the controller simply orchestrates calls to the service
Check out the before and after in the repo.
Project structure after the refactor:
com.example.spaghetti
βββ controller
β βββ ThingController.java
β βββ ItemController.java
βββ service
β βββ ThingService.java
β βββ ItemService.java
βββ repository
β βββ ThingRepository.java
β βββ ItemRepository.java
βββ model
β βββ Thing.java
β βββ Item.java
Step 2: From βUseless Beanβ to Simple Validation
In the initial version of the code, I had a leftover bean:
@Bean
public String uselessBean() {
return "I am a useless bean";
}
It did absolutely nothing β until now.
Instead of deleting it, I replaced it with a simple validation rule:
check if a name starts with an uppercase letter.
@Bean
public Predicate<String> nameStartsWithUppercaseValidator() {
return name -> name != null && !name.isEmpty() && Character.isUpperCase(name.charAt(0));
}
I then injected it into the controller and used it to validate input before saving a new item.
No big framework, no annotation magic β just a good old @bean doing something useful.
Where should this bean live?
Since this is a generic validation, not part of the domain logic itself, I moved it to a dedicated config package:
com.example.spaghetti
βββ config
β βββ MainConfig.java
According to DDD principles, reusable and infrastructure-related beans like this one shouldn’t live inside your domain or application logic.
Placing it under config (or infrastructure.config) keeps your architecture clean and responsibilities well separated.
Small win: the bean now enforces a business rule β and the code stays clean and reusable.
Even small refactors like this help keep things tidy and meaningful.
Step 3: Giving Purpose to the Utils Class
Previously, our Utils class had two static methods that werenβt actually used anywhere.
Now, we brought it to life by adding a new method that counts the number of letters in a given string:
public int countLetters(String input) {
if (input == null) return 0;
return (int) input.chars()
.filter(Character::isLetter)
.count();
}
This method helps us process input names more meaningfully.
In the controller, we call this method to log how many letters the submitted name has before saving it.
Small steps like this turn βunused helpersβ into valuable tools for our application!
Conclusion: Setting the Stage for Clean Architecture and DDD
Weβve cleaned up the code by separating responsibilities and giving purpose to unused parts like the validation bean and utils.
This foundation makes our codebase cleaner and easier to maintain.
Next, weβll introduce Clean Architecture layers and apply proper domain modeling with DDD to take our design to the next level.
Stay tuned!
This content originally appeared on DEV Community and was authored by Gabriela Goudromihos Puig