Now that our architecture is modular and our input/output boundaries are well-defined, it’s time to make the system more robust and less fragile.
Because clean code is not just about structure — it’s about behavior under pressure.
In this part of the journey, we’ll:
✅ Centralize error handling so unexpected exceptions don’t leak into the user’s face
✅ Add meaningful error messages and proper HTTP responses
✅ Start writing unit tests to protect behavior and refactors
✅ Learn how to test services, controllers, and edge cases confidently
We’ve cleaned up the house — now we’re adding locks to the doors and alarms to the windows.
Let’s make it reliable. 🛡️
🛡️ Step 1: Global Error Handling — Clean, Scoped & Scalable
A clean architecture doesn’t stop at structure — it must also handle errors predictably and clearly. Instead of generic stack traces or inconsistent messages, we now return standardized and meaningful responses across the entire API.
🔹 Scoped Handlers per Domain
We created three focused @ControllerAdvice classes:
com.example.spaghetti.adapter.exception
└── ThingExceptionHandler.java ← Handles errors from the ThingController
└── ItemExceptionHandler.java ← Handles errors from the ItemController
└── ValidationExceptionHandler.java ← Catches validation failures and generic exceptions
This structure avoids bloated global handlers and keeps each domain's behavior isolated and easy to reason about.
📦 All handlers are placed under adapter.exception, since they're part of how the application communicates with the outside world — not core business logic.
🔹 Domain Exceptions with Meaning
We introduced custom exceptions that live in the domain layer:
com.example.spaghetti.domain.exception
└── ThingNotFoundException.java
└── ItemNotFoundException.java
They extend a shared base:
com.example.spaghetti.adapter.exceptionEntityNotFoundException
This allows us to:
✅ Express business rules clearly in the domain
✅ Share logic for testing and centralized error mapping
✅ Keep the application layer free of hardcoded strings and magic messages
🔹 Ready to Scale
This setup follows Clean Architecture and DDD principles:
- Domain stays isolated — no HTTP, no annotations
- Adapter layer deals with translation to REST responses
- Each new domain can have its own exceptions and handler
💡 The result: when your app grows, your error handling doesn’t get messier — it grows with it, cleanly.
🧹 Adjusting Error Handling and Responsibilities
During the project cleanup, we removed the name validation from the ItemController:
if (!nameValidator.test(itemRequest.getName())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Name must start with uppercase");
}
This is because this validation represents a business rule, and according to Clean Architecture and DDD principles, business rules belong in the application or domain layer, not in the controller (adapter) layer.
Now, this validation lives inside the ItemService, which throws a specific exception (InvalidItemNameException) when the rule is violated. This exception is caught by the ItemExceptionHandler and translated into the proper HTTP response.
This adjustment keeps the controller lean and centralizes business logic in the right place.
📝 What about the letter count log in the ThingController?
On the other hand, we kept the letter count logging in the ThingController:
int letterCount = utils.countLetters(thingRequest.getName());
System.out.println("Name has " + letterCount + " letters.");
This code does not affect application logic or control flow — it’s just a utility for logging or debugging, with no business impact.
Therefore, it’s fine to leave it in the controller.
🧪 What’s Next: Unit and Integration Testing, with Purpose
With error handling centralized and domain boundaries protected, our system is no longer fragile — but it’s not battle-tested yet.
In the next sessions, we’ll dedicate entire steps to testing, because clean architecture without tests is like building a fortress with no guards.
🔬 We’ll start with unit tests, focusing on services and core use cases:
- Validate business logic in isolation
- Protect against regressions
- Speed up development with confidence
🔗 Then, we’ll move to integration tests, making sure:
- Controllers, mappers, and dependencies work as a system
- HTTP endpoints behave correctly under real conditions
- The whole app remains stable during changes and refactors
Testing is not a checkbox — it’s part of designing resilient software.
So in the next chapters, we’ll not only write tests — we’ll structure them with the same discipline we applied to the rest of the code.
🧱 Architecture is foundation.
🛡️ Error handling is protection.
🔁 Testing is confidence.
Let’s move forward with that confidence — and make this codebase bulletproof. 💥
Top comments (0)