Summary
The core issue is the Spring Boot fat jar’s classloader hierarchy and how it discovers @Configuration classes. When running from an IDE, the classpath is flat and contains all modules, allowing the component scan to easily pick up module-3. However, when packaged, Spring Boot’s LaunchedURLClassLoader prioritizes the jar’s own BOOT-INF/classes and nested jars over the root classpath. If module-3 is bundled as a standard jar inside BOOT-INF/lib, it is accessible, but if the application context fails to scan the specific packages (due to restrictive scanBasePackages or auto-configuration exclusion), the beans won’t load. The “ignored decorator” symptom suggests the bean definition exists but was not processed by the container during the jar execution, often because the @Configuration class was not registered or @ComponentScan missed the dependency’s package.
Root Cause
The root cause is twofold: inadequate Component Scan coverage and Spring Boot Maven Plugin packaging configuration.
- Restrictive Component Scan: The
@SpringBootApplication(scanBasePackages = {"com.product", "com.company"})explicitly limits scanning to these two packages. Ifmodule-3resides in a different package (e.g.,com.sharedorcom.infrastructure), the Spring container will never see the@Configurationclass during startup. - Jar Packaging Structure: Spring Boot repackages the application into a fat jar with a specific structure (
BOOT-INF/classes,BOOT-INF/lib). Whilemodule-3ends up inBOOT-INF/lib, theLaunchedURLClassLoaderensures thatBOOT-INF/classesis loaded first, and nested jars are loaded in isolation. If the main application (module-1) does not explicitly expose the dependency’s package or if the plugin configuration fails to includemodule-3‘s resources correctly, the runtime environment differs from the IDE environment. - Start-Class Mismatch (Less Common): If the
spring-boot-maven-pluginis not configured correctly in a multi-module hierarchy, it might pick up a differentStart-Class(perhaps from a module higher in the hierarchy), causing the wrong application context to load.
Why This Happens in Real Systems
In real-world distributed systems, this behavior is standard but often misunderstood:
- Dependency Isolation: Maven dependencies are transitive. Developers often assume that if
module-1depends onmodule-3, all beans inmodule-3are automatically visible. However, visibility is not the same as discovery. Just because the classes are in the jar doesn’t mean Spring has registered them as beans. - Build vs. Runtime: The IDE usually adds all build dependencies to the classpath root. The Maven build creates a nested structure. This disparity hides the fact that the application relies on “accidental” classpath visibility rather than explicit configuration.
- “It Works in IDE” Trap: This is a classic symptom of Implicit Behavior Dependency. The developer relies on default scanning behavior that exists in the IDE but is suppressed or altered by the strict classloading rules of the packaged jar.
Real-World Impact
- Feature Flag Failure: If the decorator is meant to enable/disable features based on configuration (as seen in the
featureEnabledvariable), the fallback behavior (original service) will execute, potentially bypassing critical logic like audit logging, security checks, or performance tracking. - Silent Degradation: The application starts successfully, but the logic behaves differently. There are no startup errors, making this a “silent” production issue that is hard to trace.
- Inconsistent Environments: Code works in Dev (where run configurations are loose) but fails in Staging/Prod (where jars are strictly executed), leading to deployment rollbacks and loss of trust in the CI/CD pipeline.
Example or Code
// module-3/src/main/java/com/shared/config/Module3Config.java
@Configuration
// CRITICAL FIX: Ensure this matches the missing package in the main app's scan
// OR remove scanBasePackages entirely from @SpringBootApplication
public class Module3Config {
@Bean
@Primary
public SomeService decoratedService(
@Qualifier("originalService") SomeService delegate,
@Value("${feature.enabled:false}") boolean featureEnabled) {
return new DecoratedService(delegate, featureEnabled);
}
}
org.springframework.boot
spring-boot-maven-plugin
com.product.module1.Module1Application
repackage
How Senior Engineers Fix It
Senior engineers enforce Explicit Dependency Management and Defensive Configuration:
- Refactor Component Scanning: The most robust fix is to remove the restrictive
scanBasePackagesin@SpringBootApplicationand rely on Classpath Scanning. If specificity is required, move the@Configurationclasses to sub-packages of the main application or ensure the main application’s scan range covers the dependency.- Fix: Change
scanBasePackagesto includecom.shared(wherever module-3 lives) or simply delete the attribute.
- Fix: Change
- Use Auto-Configuration: The “Spring Way” is to convert
module-3into a starter. Createsrc/main/resources/META-INF/spring.factories(ororg.springframework.boot.autoconfigure.AutoConfiguration.importsin Spring Boot 2.7+) and registerModule3Configthere. This ensures the configuration is loaded regardless of package scanning. - Verify Maven Repackaging: Explicitly set the
<mainClass>in thespring-boot-maven-pluginconfiguration in the final module. This guarantees that the entry point is correct and theStart-Classproperty is set accurately.
Why Juniors Miss It
- Classpath vs. Component Scan: Juniors often conflate having a jar on the classpath with Spring scanning that jar for annotations. They don’t realize that
@ComponentScanis restricted to specific packages unless told otherwise. - IDE Magic: IDEs (IntelliJ/Eclipse) automatically add all source modules to the runtime classpath, masking the need for proper
@Configurationregistration. - Configuration Oversights: They often copy-paste
@SpringBootApplicationattributes (likescanBasePackages) from other projects without understanding the consequences, creating artificial boundaries that break modularity. - Lack of Repackage Understanding: They may not understand that
mvn packagecreates a completely different artifact layout (nested jars) than a simplemvn compile, leading to the “works in IDE, fails in jar” scenario.