Home โ€บ Java โ€บ Spring Boot Project Structure: Wrong Package Causes 404 Errors
Beginner 4 min · July 14, 2026

Spring Boot Project Structure: Wrong Package Causes 404 Errors

Learn why incorrect package placement in Spring Boot leads to 404 errors.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic understanding of Spring Boot and REST APIs
  • Java 11+ installed on your machine
  • Maven or Gradle build tool familiarity
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

โ€ข 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

โœฆ Definition~90s read
What is Spring Boot Project Structure?

Spring Boot project structure is the hierarchical arrangement of Java packages and classes that determines how Spring's component scanning discovers and registers beans, with the main application class serving as the scanning root.

โ˜…
Think of your Spring Boot application as a house.
Plain-English First

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.

Consider this structure
  • 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.

MainApplication.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootApplication
public class PaymentApplication {
    public static void main(String[] args) {
        SpringApplication.run(PaymentApplication.class, args);
    }
}

// Controller in wrong package: com.example.payment.admin
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
    @GetMapping
    public String list() {
        return "Payments";
    }
}
Output
When you hit GET /api/payments, you get HTTP 404. The controller is never registered because it's not in a sub-package of the main class.
โš  The Silent Failure
๐Ÿ“Š Production Insight
In microservices with shared libraries, we once had a 6-hour outage because a junior developer moved the main class to 'com.company.payment.v2' during a version upgrade. The controllers stayed in 'com.company.payment.controller'. Always run a component scan audit after any package refactoring.
๐ŸŽฏ Key Takeaway
Always place your main application class in the root package of your project. All other packages must be sub-packages of that root.
spring-boot-project-structure Three-Layer Spring Boot Architecture Essential layers for clean project structure Controller Layer @RestController | @RequestMapping | DTOs Service Layer @Service | Business Logic | Transaction Management Repository Layer @Repository | JPA Repositories | Data Access THECODEFORGE.IO
thecodeforge.io
Spring Boot Project Structure

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.

ScanningTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootTest
class ComponentScanTest {
    @Autowired
    private ApplicationContext context;
    
    @Test
    void verifyControllerScanned() {
        String[] beans = context.getBeanDefinitionNames();
        boolean found = Arrays.stream(beans)
            .anyMatch(name -> name.contains("paymentController"));
        assertTrue(found, "PaymentController not in context!");
    }
}
Output
If the test fails, you know immediately that your controller isn't being scanned. Run this after any package refactoring.
๐Ÿ”ฅDebug Tip
๐Ÿ“Š Production Insight
In a real-time analytics system, we had a module with 20 controllers that all returned 404 after a Maven module split. The root cause: the new module had its own main class, but the controllers were in a package that wasn't a sub-package of that new main class. We fixed it by adding @ComponentScan on the new main class.
๐ŸŽฏ Key Takeaway
@SpringBootApplication does three things: @Configuration, @EnableAutoConfiguration, @ComponentScan. Know exactly what you're losing if you replace it.

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.

FixedApplication.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
@ComponentScan(basePackages = {
    "com.example.app",
    "com.example.payment.controller",
    "com.example.utils"
})
public class PaymentApplication {
    public static void main(String[] args) {
        SpringApplication.run(PaymentApplication.class, args);
    }
}

// Now the controller in com.example.utils will be found
Output
After adding @ComponentScan with the correct packages, the 404 resolves. The controller is now registered and responds to requests.
โš  Explicit Override
๐Ÿ“Š Production Insight
In a SaaS billing system, we had a 'shared' library that contained common DTOs and utilities. The library's package was 'com.company.billing.shared', while the main app was 'com.company.billing.api'. We added @ComponentScan("com.company.billing.shared") on the main class, and all beans from the shared library were discovered.
๐ŸŽฏ Key Takeaway
Use @ComponentScan(basePackages = {...}) for explicit control, but always include the main class's package in the list.
spring-boot-project-structure Correct vs Wrong Package Structure Impact on Spring Boot application behavior Correct Structure Wrong Structure Base Package com.example.project com.example.wrong Component Scan Scans all sub-packages Misses controller package Endpoint Access Returns 200 OK Returns 404 Not Found DTO Placement In controller or shared package Scattered across packages Build Tool Gradle with proper dependencies Maven with misconfigured plugins THECODEFORGE.IO
thecodeforge.io
Spring Boot Project Structure

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.

DataModuleConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
// In the 'data' module
@Configuration
@ComponentScan("com.company.data")
public class DataModuleConfig {
    @Bean
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }
}

// In 'META-INF/spring.factories' of data module
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.company.data.DataModuleConfig
Output
The main application module automatically picks up the DataModuleConfig from the data module JAR via spring.factories. No explicit @ComponentScan needed on the main class.
๐Ÿ”ฅSpring Boot 3.0+ Change
๐Ÿ“Š Production Insight
A payment gateway project had 5 Maven modules. The 'risk' module's beans were never found because we relied on @ComponentScan on the main class. Switching to spring.factories fixed it and made the architecture cleaner.
๐ŸŽฏ Key Takeaway
In multi-module projects, use spring.factories or AutoConfiguration.imports to register beans from other modules. Avoid @ComponentScan across JAR boundaries.

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.

EndpointSmokeTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class EndpointSmokeTest {
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    void paymentEndpointShouldReturn200() {
        ResponseEntity<String> response = restTemplate
            .getForEntity("/api/payments", String.class);
        assertEquals(200, response.getStatusCodeValue());
    }
    
    @Test
    void healthEndpointShouldBeUp() {
        ResponseEntity<String> response = restTemplate
            .getForEntity("/actuator/health", String.class);
        assertEquals(200, response.getStatusCodeValue());
    }
}
Output
If the endpoint returns 404, the test fails immediately. Run this as part of your CI pipeline to catch package structure issues.
โš  Test Package Matters
๐Ÿ“Š Production Insight
In a real-time analytics pipeline, we added a smoke test suite that runs against every deployment. It caught a 404 on a critical data ingestion endpoint that would have caused a 30-minute data loss.
๐ŸŽฏ Key Takeaway
Write integration tests that verify all critical endpoints return 200. Run them in CI to catch package structure issues early.

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:

FilteredScanConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@ComponentScan(
    basePackages = "com.company.app",
    includeFilters = @ComponentScan.Filter(
        type = FilterType.ANNOTATION,
        classes = RestController.class
    ),
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.REGEX,
        pattern = ".*Legacy.*"
    )
)
public class ScanConfig {
    // Only @RestController beans from com.company.app are scanned
    // Beans with 'Legacy' in the name are excluded
}
Output
Only beans annotated with @RestController are scanned. Any bean with 'Legacy' in its fully qualified class name is excluded. This gives you surgical control over scanning.
๐Ÿ”ฅFilter Performance
๐Ÿ“Š Production Insight
In a legacy migration project, we used excludeFilters to prevent old @Service beans from being scanned while we incrementally replaced them. This allowed a gradual rollout without breaking existing functionality.
๐ŸŽฏ Key Takeaway
Use @ComponentScan filters to include or exclude specific beans. Prefer annotation-based filters over regex for performance.

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:

IdealStructure.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Main class in com.company.app
@SpringBootApplication
public class AppApplication {
    public static void main(String[] args) {
        SpringApplication.run(AppApplication.class, args);
    }
}

// Controller in com.company.app.controller
@RestController
@RequestMapping("/api/users")
public class UserController {
    @GetMapping
    public List<User> getAll() {
        return List.of();
    }
}

// Service in com.company.app.service
@Service
public class UserService {
    // Business logic
}
Output
Everything is discovered automatically. No explicit @ComponentScan needed. The 404 problem is eliminated by design.
๐Ÿ”ฅNaming Convention
๐Ÿ“Š Production Insight
In a startup's SaaS platform, we enforced this structure via architecture tests using ArchUnit. Any commit that violated the package hierarchy failed the build. It prevented 404 issues in production entirely.
๐ŸŽฏ Key Takeaway
Keep the main class in the root package. All other packages must be sub-packages. This is the simplest and most reliable structure.

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.

EndpointDiagnostic.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/internal/diagnostic")
public class EndpointDiagnostic {
    @Autowired
    private RequestMappingHandlerMapping handlerMapping;
    
    @GetMapping("/mappings")
    public Map<String, List<RequestMappingInfo>> getAllMappings() {
        return handlerMapping.getHandlerMethods()
            .entrySet().stream()
            .collect(Collectors.groupingBy(
                e -> e.getValue().getBeanType().getSimpleName(),
                Collectors.mapping(
                    e -> e.getKey(),
                    Collectors.toList()
                )
            ));
    }
}
Output
Hitting GET /internal/diagnostic/mappings returns a JSON map of all registered endpoint mappings. If your controller isn't in the map, it's not being scanned.
โš  Don't Expose in Production
๐Ÿ“Š Production Insight
A client had a 404 on a payment callback endpoint for weeks. It turned out a security filter was matching the path and returning 404 before the controller could handle it. We added a breakpoint in the filter chain to trace the issue.
๐ŸŽฏ Key Takeaway
When 404s persist, check annotations, duplicate mappings, security filters, and proxy configuration. Use the diagnostic endpoint to verify mappings are registered.
● Production incidentPOST-MORTEMseverity: high

The Silent 404 That Took Down a Payment Endpoint

Symptom
All REST endpoints in the 'payment' module returned 404, while other modules worked fine.
Assumption
The team assumed the controller annotations or request mappings were wrong.
Root cause
A developer moved the main application class to a new package 'com.company.payment.v2' while the controllers stayed in 'com.company.payment.controller'. The new main class package had no sub-packages with controllers.
Fix
Moved the main application class back to 'com.company.payment' and added @ComponentScan("com.company.payment.controller") as a temporary measure during refactoring.
Key lesson
  • 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
Production debug guideStep-by-step process to diagnose and fix 404 errors caused by package structure issues3 entries
Symptom · 01
All endpoints in a specific module return 404
Fix
Check if the module's beans are in a sub-package of the main application class. If not, add @ComponentScan or use spring.factories.
Symptom · 02
Endpoints work locally but fail in production
Fix
Compare the package structure between local and production builds. Check for differences in Maven module configuration or classpath.
Symptom · 03
Some endpoints work, others don't
Fix
Use the diagnostic endpoint to list all registered mappings. Identify which controllers are missing and trace their package locations.
★ Quick Debug Cheat Sheet for Spring Boot 404Immediate steps to diagnose and fix 404 errors from package structure issues
Controller not found (404 on all endpoints)
Immediate action
Verify the controller's package is a sub-package of the main class
Commands
Check package structure: main class in com.company.app, controller in com.company.app.controller or similar
Add logging: logging.level.org.springframework.boot.autoconfigure=DEBUG
Fix now
Move controller to correct package or add @ComponentScan("com.company.controller")
Multi-module project 404+
Immediate action
Check if the module has a spring.factories file
Commands
Look for META-INF/spring.factories in the module JAR
Verify the configuration class is listed in the file
Fix now
Add spring.factories or AutoConfiguration.imports to the module
ApproachBest For
Default @SpringBootApplication scanningSingle-module projects with all code under one root package
Explicit @ComponentScan with basePackagesProjects that need to scan multiple package roots
spring.factories / AutoConfiguration.importsMulti-module Maven/Gradle projects
@Import on configuration classesSelectively importing specific configurations from other modules
⚙ Quick Reference
8 commands from this guide
FileCommand / CodePurpose
MainApplication.java@SpringBootApplicationThe Root Cause
ScanningTest.java@SpringBootTestWhat the Official Docs Won't Tell You
FixedApplication.java@SpringBootApplicationFixing the 404
DataModuleConfig.java@ConfigurationMulti-Module Maven/Gradle Projects
EndpointSmokeTest.java@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)Testing Your Package Structure
FilteredScanConfig.java@ConfigurationAdvanced
IdealStructure.java@SpringBootApplicationThe Package Structure That Works
EndpointDiagnostic.java@RestControllerTroubleshooting

Key takeaways

1
Place your main application class in the root package of your project to ensure all sub-packages are scanned.
2
Use @ComponentScan explicitly only when you need to scan packages outside the main class's hierarchy.
3
In multi-module projects, use spring.factories or AutoConfiguration.imports to register beans across modules.
4
Write integration tests that verify all critical endpoints return 200 to catch package structure issues early.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What happens if you place a @RestController outside the main application...
Q02SENIOR
How would you design a multi-module Spring Boot project to avoid compone...
Q03SENIOR
Explain the difference between @SpringBootApplication and @EnableAutoCon...
Q01 of 03JUNIOR

What happens if you place a @RestController outside the main application class's package hierarchy?

ANSWER
The controller will not be discovered by component scanning, resulting in 404 errors for all its endpoints. Spring Boot logs a debug message about the missing bean, but no error is thrown. The fix is to either move the controller to a sub-package or add @ComponentScan with the correct package.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Why does Spring Boot return 404 when my controller is in the wrong package?
02
Can I use @ComponentScan on multiple classes?
03
How do I fix 404 in a multi-module Maven project?
04
Does Spring Boot scan JAR files in the classpath?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

4 min read · try the examples if you haven't

Previous
Spring Boot Application Properties Explained
2 / 121 · Spring Boot
Next
Spring Boot Auto-Configuration Explained