π§ 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! π
Top comments (0)