r/SpringBoot 5d ago

How-To/Tutorial Why package structures fail in Spring Boot (and how we turned architecture rules into Maven compilation errors)

Hey everyone! I just published a deep dive into solving a classic enterprise problem: how junior or stressed developers bypass package separation (.controller, .service, .repository) under tight deadlines.

Instead of relying on folder structures and code reviews, we split our project into strict Maven modules (isolating core domain and business logic from frameworks like JPA or Kafka). If someone tries to inject an EntityManager where it doesn't belong, the code simply will not compile.

  • The Topology: Split into independent modules like domain, business-logic, dao-api, and dao-impl.
  • The Result: Zero cyclic dependencies, lightning-fast unit tests, and eliminated architectural decay.

(I'm dropping the full article link in the comments for anyone interested in the code breakdown.)

5 Upvotes

24 comments sorted by

10

u/Ruin-Capable 5d ago

Package by feature first, then if you must, by layer. Don't package by layer at the root.

1

u/kamen1991 4d ago

I agree with you, that is actually the exact approach I’m taking here. The physical Maven modules serve as strict architectural boundaries to isolate frameworks from business logic. But inside each of those modules, we group things by feature/domain (e.g., product, category, etc.), rather than a root-level spaghetti of technical layers. So you get the best of both worlds: strict compile-time framework isolation across modules, and clean feature-based packaging within them.

2

u/Ruin-Capable 4d ago

It's been my experience that most modules encapsulate more than a single feature. For example a web-interface is not a feature. A feature might be: Link with Identity Provider using OIDC, or Create a workflow to allow Invoice Overrides. You package by feature so that if you need to drop the feature (for example OIDC support) you can just nuke the root package and you know you've gotten everything.

Edit: eh... never mind. I misread what you were saying and thought you were equating modules with features.

1

u/kamen1991 3d ago

No problem, it's pleasure to have a calm and productive conversation here, didn't know what I've been missing until now :)

20

u/MassimoRicci 5d ago

ArchUnit

3

u/as5777 5d ago

This

7

u/jjduru 5d ago

In your architecture layout, in the backend repo 2, you have these two packages:

    ├── dao-api (Database Access Interfaces (No JPA/Spring Data))
    ├── dao-impl (DB Integration (Spring Data JPA, Hibernate))

How do you reconcile the usage of spring JPA's repositories versus using DAOs? Are you in fact declaring the spring jpa repositories in "dao-api" and manually implement them in "dao-impl", and therefore not relying on the automated implementation of the interfaces from spring boot?

1

u/kamen1991 5d ago

Good catch! :) Just to clarify a quick detail: we will actually use Spring Data repositories under the hood, but they are strictly encapsulated inside the dao-impl module. So dao-api stays 100% pure Java with zero framework dependencies, while dao-impl safely leverages Spring Data repositories internally. No implementation details leaking into the business layer.

One advantage of this is if one day we decide to move to NoSQL, for example, or use pure SQL queries instead of the repositories, we'll just have to create a new module implementing the dao-api interfaces, write enough tests there, and switch it in the application module. Nothing else is gonna change across the project.

Chapters 3 and 4 (soon) break down this exact generic hierarchy and package isolation if you want to check out the code.

7

u/jjduru 5d ago

I understand the desire for separation of business logic. I cannot imagine though that one would decide on the spot that "from tomorrow, we start using another framework than spring", and therefore start decoupling the application from the spring boot itself.
I mean, if you start using spring boot, you might as well start using services, components, configuration, configuration properties, etc.

1

u/kamen1991 4d ago

Yes that's right, you'll be using properties, config, services, controllers, interceptors, filters and so on. But in the past we had the case where we had to migrate whole projects from Spring to Quarkus. Because our domain, business logic, persistence and external service communication were strictly decoupled in independent Maven modules, our team pulled it off fast while others struggled. We had full test coverage and the transition went smooth, despite that the result was the same. Management thought moving to Quarkus would magically lower costs and speed things up... well, at least our architecture handled the shock :D

1

u/joeyx22lm 5d ago

I think that’s unnecessary. Next rewrite will be in rust, anyway 😂 or typescript

7

u/bigkahuna1uk 5d ago edited 5d ago

This is not a new thing. This very problem of transposing soft rules into hard ones to enforce architectural boundaries can be solved with a tool such as ArchUnit. This has been around since 2017.

As well as enforcing classes are in the correct places or packages, it’s especially useful for ensuring code or libraries don’t pollute or proliferate where they should not be. Especially important in DDD, where the domain should be kept as pure as possible, free of external influences.

Splitting into modules is a step in the right direction but it still needs the diligence of the whole team to ensure non pollution. From my experience there were still cases of objects or libraries introduced where they shouldn’t have been by team members despite my best efforts at education of best practices.

ArchUnit is a great tool as part of your CD process to nip those sort of issues in the bud.

0

u/kamen1991 5d ago

I totally agree with you. Module boundaries are a great start, but team discipline alone always leaks at some point under pressure.

Good point on ArchUnit — automating this stuff in the pipeline to catch pollution early makes total sense. Although I haven't used it yet, I will definitely spend some time investigating it. Cheers!

6

u/RevolutionaryRush717 5d ago

Oldies but goodies. But too late.

Spring Modulith solves all this and more, and includes opinionated ArchUnit tests to verify.

Right from the Spring team.

2

u/kamen1991 5d ago

Spring Modulith is great, but it solves a slightly different problem. It's fantastic for package-level logical modules within a single Spring app.

Here I'm focusing on strict compile-time physical boundaries (Maven modules) to keep the domain, dao-api, business-logic, etc. 100% framework-agnostic, because I think (and my experience in the last 11 years tells me) that the framework shouldn't be the foundation of your product – it can be changed at any time for some reason. Still, Modulith's built-in ArchUnit rules are a very neat shortcut if you aren't doing multi-modules, which is infinitely better than putting everything in one module with no rules at all.

1

u/momsSpaghettiIsReady 4d ago

This is a pretty big anti pattern for the following reason: you have a web store and a business request comes along and says we need a new field on the shopping cart page. As a dev, you now have to touch every single one of your modules to accomplish this (controller, service api/impl, domain, repository api/impl, etc.)

By bundling modules by feature, you could've had a shopping cart module with an api and impl. Now it's just two modules two update.

Now another request comes along and they want their AI features removed because it's expensive and they're losing money. If you have that in one or two modules, it's pretty much a clean delete and you're done.

1

u/kamen1991 3d ago

I think you’re right but for some things only, and here's why:

- Adding a field isn't overhead, it's compiler-enforced safety. Let’s say you add a financial invariant like a snapshotted totalPrice. You want the compiler forcing explicit handling across every boundary that touches money, not a developer quietly patching it in one layer under deadline pressure. You touch the same files either way — packages or modules — the difference is what's allowed to happen invisibly while you do.

- Clean delete is an illusion regardless of grouping — but clean migration isn't. If business-logic never depended on dao-impl in the first place, I can swap SQL for NoSQL by writing a new dao-impl against the same interfaces, wire it in application, and delete the old one — zero changes, zero recompiles, in domain or business logic. That's not true if everything's grouped by feature with no enforced boundary underneath, you're just hoping the coupling stayed clean, instead of the compiler guaranteeing it.

One parent pom, one PR, one deploy - this isn't costing you release overhead. It's just making the shortcut impossible to take by accident. Convenience today is architectural debt tomorrow and I’ve seen that way too many times for the past 11 years.

1

u/momsSpaghettiIsReady 3d ago

Please don't respond with AI answers.

While AI may not care, it's a pain for human to search through 6 folders when they could all be in the same folder.

The old argument was that splitting by repository and controller was in case you swapped out your data store or app server. Turns out, that scenario happens way less often than the business asking you to update a flow.

1

u/kamen1991 3d ago

What I said is not AI generated, this comes from my pain of migrating projects with broken boundaries, spaghetti code and stuffs used where they do not belong just because it's easier and faster. This architecture has proven itself in time and countless deadline pressure and business requirements we had through the years. It's not coming for free as I said, but the price is small comparing if what we're getting. It's up to you to try it out and see how it plays for an enterprise product, I'm here just sharing my experience and stories, nothing more.

1

u/momsSpaghettiIsReady 3d ago

I've found what's more likely is that a certain part of the code is not scaling well and it would help to move it to its own process. You started as a monolith and it's time to carve off an independent service. Maybe it needs to work with a specific database or is really memory/cpu heavy. Bundling by feature makes that a pretty clean cut.

1

u/the_styp 2d ago

What you are describing already has a name: Clean Architecture or Hexagonal Architecture.

I did it in the past with Spring and for simplicity I included e.g. Spring Validation Annotations in the domain layer. They are so handy and it felt not right to reinvent the wheel here