Spring Boot Project Structure: Wrong Package Causes 404 Errors
Learn why incorrect package placement in Spring Boot leads to 404 errors.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- ✓Basic understanding of Spring Boot and REST APIs
- ✓Java 11+ installed on your machine
- ✓Maven or Gradle build tool familiarity
โข Place your main application class in the root package above all other packages โข Spring Boot auto-configuration scans only sub-packages of the main class โข Controllers, services, and repositories outside the root package cause 404 errors โข Use @ComponentScan explicitly if you must break this rule โข Maven/Gradle module structure must mirror package hierarchy
Think of your Spring Boot application as a house. The main application class is the front door โ everything inside the house must be reachable through that door. If you put your controllers in a separate building out back, no one can find them, and you get a 404 โ the house is empty from the visitor's perspective.
You've just spent hours writing a beautiful REST controller with all the right annotations. You fire up your Spring Boot application, hit the endpoint, and boom โ 404. Not Found. The controller exists, the mapping is correct, but Spring Boot acts like it's invisible. If you've been in the Java ecosystem long enough, you've seen this before. It's almost always a package structure problem.
Spring Boot relies on component scanning to discover beans. By default, it scans from the package containing the main application class and all sub-packages. Place your controller in a sibling package, and it's dead code. This isn't a bug โ it's by design, but it catches even experienced developers when they refactor or split modules.
In this article, we'll dissect exactly why this happens, how to fix it, and how to structure your projects to avoid this issue entirely. We'll cover real production scenarios, including multi-module Maven projects and the infamous 'I put it in the right package but it still doesn't work' problem. By the end, you'll never waste hours on a 404 caused by package misplacement again.
The Root Cause: Component Scanning and Package Hierarchy
Spring Boot's auto-configuration is powerful, but it has a fundamental assumption: your main application class sits at the root of your package tree. When you annotate a class with @SpringBootApplication, it implicitly includes @ComponentScan, @EnableAutoConfiguration, and @Configuration. The @ComponentScan annotation, by default, scans the package of the annotated class and all sub-packages.
- com.example.app (main class here)
- com.example.app.controller (sub-package, scanned)
- com.example.app.service (sub-package, scanned)
- com.example.utils (sibling package, NOT scanned)
If you place a controller in com.example.utils, it will never be discovered. Spring Boot logs a warning at debug level, but in production, you'll just see 404s. This is the number one cause of 'missing bean' issues in Spring Boot applications.
Let's see this in action with a concrete example.
What the Official Docs Won't Tell You
The official Spring Boot documentation tells you to put the main class in the root package. What it doesn't tell you are the edge cases that will burn you in production. First, the default scanning behavior applies to @SpringBootApplication, but if you use @EnableAutoConfiguration directly, you lose the implicit @ComponentScan. I've seen teams switch to @EnableAutoConfiguration for 'more control' and then wonder why nothing works.
Second, the scan is recursive only for sub-packages. If you have a package structure like 'com.example.app' and 'com.example.app.controller.admin', that works. But 'com.example.app' and 'com.example.other' does not โ even though they share the first two segments. The scan is directory-based, not name-based.
Third, in multi-module Maven projects, each module has its own main class. If you have a 'common' module with shared utilities, those classes won't be scanned unless you explicitly add @ComponentScan or use Spring Factories. This is a common source of 'works in IDE, fails in production' bugs.
Here's a test to verify your scanning scope:
Fixing the 404: Explicit @ComponentScan Configuration
When you can't restructure your packages โ for example, when integrating third-party libraries or legacy code โ you need explicit @ComponentScan. This annotation accepts basePackages or basePackageClasses. The latter is type-safe and refactoring-friendly.
Here's the pattern: on your main application class, add @ComponentScan with an array of package names. You can also scan multiple packages. Be careful: if you specify basePackages, the default scanning from the main class package is overridden. You must include the main class's package explicitly if you still want it scanned.
A common mistake is to use @ComponentScan on a configuration class instead of the main class. It works, but it's confusing. Keep all scanning configuration on the main class or in a dedicated @Configuration class that the main class imports.
Let's see how to fix our earlier problem:
Multi-Module Maven/Gradle Projects: The Real Challenge
Enterprise applications rarely live in a single Maven module. You typically have a parent POM with modules like 'api', 'core', 'data', 'common'. Each module is a separate JAR, and Spring Boot's auto-configuration doesn't cross JAR boundaries by default.
When you have a multi-module project, the main application class lives in one module (usually 'api' or 'application'). Beans in other modules are not scanned unless you explicitly configure it. The solution is to use Spring's @Import or AutoConfiguration.imports file.
Spring Boot 2.7+ uses 'spring.factories' or 'META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports' to register auto-configuration classes from other modules. This is the proper way to share beans across modules without @ComponentScan.
Here's a common multi-module structure and how to wire it:
Testing Your Package Structure: Integration Tests That Catch 404s
The best way to prevent 404s from package issues is to write tests that verify your endpoints are reachable. A simple integration test using @SpringBootTest with a web environment can hit every endpoint and check for 200 status codes. This catches the problem before it reaches production.
But there's a catch: if your test class is in the wrong package, it might not scan the beans either. Always place your test class in the same package as the main application class, or use @ContextConfiguration to specify the correct configuration.
Here's a robust test pattern that I use in every project:
Advanced: Customizing Component Scanning with Filters
Sometimes you need fine-grained control over which beans are scanned. Spring's @ComponentScan supports includeFilters and excludeFilters. This is useful for excluding test beans from production, or including only specific stereotype annotations.
For example, you might have a package with both controllers and services, but you only want to scan controllers. Or you might want to exclude a specific bean that conflicts with another. Filters use AOP-style pointcut expressions or annotation types.
Here's a real example from a project where we had to exclude a legacy bean:
The Package Structure That Works: A Proven Template
After years of trial and error, I've settled on a package structure that minimizes 404 issues and scales well. The key principle: the main application class is the root, and every functional area is a sub-package. Here's the template:
- com.company.app (main class)
- com.company.app.config (configuration classes)
- com.company.app.controller (REST controllers)
- com.company.app.service (business logic)
- com.company.app.repository (data access)
- com.company.app.dto (data transfer objects)
- com.company.app.exception (custom exceptions)
- com.company.app.util (utility classes)
Each sub-package can have further sub-packages. For example, 'controller' might have 'v1' and 'v2' for API versioning. The key is that everything is under 'com.company.app'.
For multi-module projects, the structure is similar but each module has its own root: - api: com.company.api - core: com.company.core - data: com.company.data
And you use spring.factories to cross-wire them. Let's see a complete example:
Troubleshooting: When 404s Persist Despite Correct Structure
You've verified the package structure is correct, the main class is in the root, and all controllers are in sub-packages. But you still get 404s. What now?
First, check for annotation issues. A missing @RestController or @RequestMapping will cause 404s. Second, check for duplicate request mappings โ if two controllers map to the same path, Spring Boot might pick one and ignore the other. Third, check for security filters that block the endpoint. Spring Security can return 404 instead of 401 if the filter chain doesn't match.
Fourth, check for proxy configuration issues. If you're behind a load balancer or API gateway, the path might be rewritten. Use X-Forwarded-Prefix or server.forward-headers-strategy to fix this.
Here's a diagnostic endpoint you can add to your application:
The Silent 404 That Took Down a Payment Endpoint
- Never move the main application class without auditing all bean locations
- Use @ComponentScan explicitly in multi-module projects
- Add integration tests that verify all expected endpoints are reachable
Check package structure: main class in com.company.app, controller in com.company.app.controller or similarAdd logging: logging.level.org.springframework.boot.autoconfigure=DEBUG| File | Command / Code | Purpose |
|---|---|---|
| MainApplication.java | @SpringBootApplication | The Root Cause |
| ScanningTest.java | @SpringBootTest | What the Official Docs Won't Tell You |
| FixedApplication.java | @SpringBootApplication | Fixing the 404 |
| DataModuleConfig.java | @Configuration | Multi-Module Maven/Gradle Projects |
| EndpointSmokeTest.java | @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) | Testing Your Package Structure |
| FilteredScanConfig.java | @Configuration | Advanced |
| IdealStructure.java | @SpringBootApplication | The Package Structure That Works |
| EndpointDiagnostic.java | @RestController | Troubleshooting |
Key takeaways
Interview Questions on This Topic
What happens if you place a @RestController outside the main application class's package hierarchy?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Spring Boot. Mark it forged?
4 min read · try the examples if you haven't