Most backend codebases become hard to change for the same reason. The business logic knows too much about the infrastructure. After working with Spring Boot services in production, I've seen this pattern consistently. A service that started clean slowly becomes a system where changing the database means touching the core logic. Where adding a new endpoint breaks something unrelated. Where testing requires spinning up half the infrastructure. Hexagonal Architecture solves exactly that. The idea is simple: your core domain should not depend on anything external. Not the database. Not the HTTP layer. Not Kafka. Not Spring annotations scattered through your business logic. What that means in practice: • Your use cases are pure Java. No framework dependencies. • Ports define what your application needs, not how it gets it. • Adapters handle the external world: REST, Kafka, JPA, anything. What changes when you apply it: • You can test your business logic without a database • You can swap an adapter without touching the domain • New developers understand where business rules live The mistake most teams make is treating it as over-engineering. It's not. It's separating what your system does from how it does it. When your domain depends on Spring, you don't have a domain. You have infrastructure with business logic mixed in. Are you applying any of this in your current codebase? #Backend #Java #SpringBoot #HexagonalArchitecture #SoftwareEngineering #SystemDesign #CleanArchitecture
Clean domain, low swap cost, and unit tests that actually test business logic, not mocks of mocks. The hardest part in practice is keeping the team aligned on not leaking Spring into the domain. The architecture doesn't enforce itself.
In fact, my team and I are currently leading an architectural migration. We’ve identified several systemic issues in the previous design, particularly around tight coupling between business logic and infrastructure components.
There are 3 golden Rules - to achieves the next level of readability - so that the code tells a story. 1. Packages should never depend on sub-packages. 2. Sub-packages should not introduce new concepts, just more details. 3. Packages should reflect business-concepts, not technical ones. It's fine to use subpackages within other packages, as long as they aren't at the same hierarchy level. Be mindful of cyclic dependencies. The trick is to focus on the level "0" by placing the interfaces/abstract classes/value objects/entities of the main concepts there, without technical clutter. The packages implementations for these main concepts. ├── src/ | ├─ application/ | ├─ customer/ | ├─ payment/ | ├─ inventory/ | ├─ shipping/ | ├─ Customer.java | ├─ Product.java | ├─ Order.java | ├─ Payment.java | ├─ Shipment.java | └─ ShopApplication.java Test: Can we see what the above app is about? https://github.com/andreas-wagner-dev/object-oriented-learning-journey/blob/main/blog/_draft/business_context_driven_carrental_c_sharp_en.md https://www.garudax.id/pulse/business-context-driven-ui-andreas-wagner-e6dcf/ https://www.garudax.id/pulse/decorator-pattern-where-business-logic-meets-clean-code-wagner-b8xbf