I've been writing software for over 10 years now. I've seen codebases that started clean and became unmaintainable. I've inherited legacy systems that somehow still worked despite violating every principle in the book. And I've watched teams argue for hours about folder structures while missing the entire point of clean architecture.
Let me tell you something: the confusion is real, and it's not your fault.
The Framework Problem
When you start a Spring Boot project, the framework practically begs you to do this:
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String customerEmail;
@OneToMany(mappedBy = "order")
private List<OrderItem> items;
}
@Service
public class OrderService {
@Autowired
private OrderRepository repository;
public Order save(Order order) {
return repository.save(order);
}
}
This works. It's fast to write. Your DevOps team is happy because you delivered quickly. But here's the problem: your business logic now knows about JPA, Jackson, and probably a dozen other framework concerns.
Clean architecture says your domain shouldn't know about these things. But Spring Boot is right there, making it so easy to just add one more annotation.
The Package-by-Layer Trap
Most tutorials teach you this structure:
src/main/java/
├── controller/
├── service/
├── repository/
└── model/
It feels natural. It's what everyone does. But it completely misses the point of clean architecture.
The problem isn't the folders—it's that this structure encourages you to think in terms of technical layers instead of business capabilities.
When you organize by feature instead:
src/main/java/
├── order/
│ ├── domain/
│ ├── application/
│ └── infrastructure/
├── product/
└── customer/
Something interesting happens: you start thinking about what the system does rather than what technologies it uses.
What I Learned Building Real Systems
In previous roles, I've worked on high-throughput billing systems and real-time data platforms. I've also architected multi-tenant public administration systems. Here's what actually mattered:
1. Business Logic Should Be Boring
Your core business rules shouldn't care about:
- How data is stored (PostgreSQL? MongoDB? Firestore?)
- How requests arrive (REST? GraphQL? Message queue?)
- Which framework you're using
When I needed to migrate a system from MongoDB to PostgreSQL, the pain level told me everything about the architecture quality. If your business logic is tangled with database annotations, that migration becomes a nightmare.
2. Interfaces Without Purpose Are Noise
Don't create interfaces just because someone said "clean architecture needs ports and adapters." Create them when you actually need to swap implementations or isolate dependencies.
I've seen codebases with repository interfaces that have exactly one implementation, forever. That's not architecture—that's ceremony.
3. The Real Test Is Change
Can you:
- Switch from PostgreSQL to another database without touching business logic?
- Change your REST API to use GraphQL without rewriting services?
- Replace Spring Boot with Micronaut (hypothetically)?
If the answer is "yes," you probably have decent architecture. If the answer is "are you insane?", then the folder structure doesn't matter—you're coupled.
The Practical Middle Ground
Here's what I actually do in production systems:
Keep business rules clean:
public class CancelOrderUseCase {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
public void execute(Long orderId, String reason) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFound(orderId));
if (order.isAlreadyShipped()) {
throw new CannotCancelShippedOrder(orderId);
}
if (order.isPaid()) {
paymentGateway.refund(order.getPaymentId());
}
order.cancel(reason);
orderRepository.save(order);
}
}
Notice: no @Service, no @Transactional, no JPA annotations. Just business logic.
Adapt at the boundaries:
@Service
@Transactional
public class OrderService {
private final CancelOrderUseCase useCase;
public void cancelOrder(Long orderId, String reason) {
useCase.execute(orderId, reason);
}
}
Now the Spring stuff lives at the edge. The business logic doesn't know about it.
Stop Overthinking, Start Measuring
Instead of debating folder structures, ask yourself:
How hard is it to test my business logic? If you need to spin up a database or mock 15 dependencies, something's wrong.
How often do framework updates break my code? When Spring Boot 4.0 dropped, how many files did you have to touch?
Can a new developer understand what the system does? If they have to read through controller → service → repository → entity just to understand one feature, your architecture is hiding the business logic.
What Actually Matters
After building systems across different industries—from financial services to public administration and beyond—here's my honest take:
Clean architecture isn't about folders. It's about keeping your business logic independent from the frameworks and tools that deliver it.
You don't need perfect hexagonal architecture on day one. You need to make sure that when requirements change (and they will), you can adapt without rewriting everything.
The confusion exists because we focus on the wrong things. We argue about package names while our domain models are covered in @Entity annotations. We create elaborate folder structures while our business logic lives inside Spring controllers.
Start simple. Keep your business rules clean. Test them without mocking the world. Everything else is just details.
Top comments (0)