Spring vs Spring Boot: Key Differences and Migration Path from XML to Auto-Config
Learn the core differences between Spring Framework and Spring Boot, including auto-configuration, dependency management, and a step-by-step migration guide for legacy applications..
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
- ✓Experience with Spring Framework 5.x or 6.x (XML or Java config)
- ✓Basic understanding of Maven or Gradle dependency management
- ✓Java 17+ installed on your machine
- ✓Familiarity with application.properties or YAML configuration
- ✓Preferably a legacy Spring MVC project to practice migration
• Spring Framework is the core DI and AOP container; Spring Boot is Spring plus auto-configuration, embedded servers, and opinionated defaults. • Spring Boot drastically reduces boilerplate XML/annotation configuration by auto-configuring beans based on classpath dependencies. • Migration from Spring to Spring Boot involves replacing DispatcherServlet XML with auto-configuration, moving from web.xml to embedded Tomcat, and leveraging spring-boot-starter dependencies. • Spring Boot 3.x requires Java 17+ and removes legacy features like Spring MVC XML configuration in favor of Java config. • Key migration pain points include handling custom PropertySourcesPlaceholderConfigurer, excluding auto-configuration classes, and dealing with transitive dependency conflicts.
Think of Spring Framework as a box of LEGO bricks with a manual telling you exactly how to build a house. Spring Boot is the same LEGO set but with pre-built walls, windows, and doors — you just snap them together. You can still customize everything, but 80% of the time you don't need to. In production, Spring Boot is like having a contractor who already knows your local building codes — you focus on the unique features of your house, not on wiring every socket.
If you've been in the Java ecosystem for more than a decade, you remember the dark ages of Spring: XML files hundreds of lines long, endless context:component-scan base-package attributes, and the dreaded NoSuchBeanDefinitionException at 2 AM. Spring Framework 2.5 introduced annotations, Spring 3.0 brought Java config, but the real revolution came with Spring Boot 1.0 in 2014. Today, Spring Boot dominates 90% of new Spring projects, yet many enterprises still run legacy Spring MVC applications on Tomcat with XML configuration. This article is for the engineers stuck maintaining those monoliths — or for anyone who wants to understand why Spring Boot isn't just 'Spring with magic.' We'll dissect the architectural differences, walk through a real migration from Spring 5.x to Spring Boot 3.x, and show you the production pitfalls that the official docs gloss over. You'll learn how auto-configuration actually works under the hood, why spring.factories files matter, and how to debug the infamous 'Failed to configure a DataSource' error that has haunted every Spring Boot developer. By the end, you'll have a concrete migration strategy for your legacy projects, complete with code examples from a payment-processing domain.
Core Architecture: Spring Container vs Spring Boot Auto-Configuration
At its heart, Spring Framework is an Inversion of Control (IoC) container that manages beans through dependency injection. You define beans via XML (beans.xml), annotations (@Component, @Service), or Java configuration (@Configuration, @Bean). The container reads these definitions, resolves dependencies, and creates the object graph. Spring Boot doesn't replace this — it sits on top, adding auto-configuration classes that conditionally register beans based on what's in your classpath. The key class is AutoConfigurationImportSelector, which reads spring.factories files from every spring-boot-autoconfigure JAR. For example, if spring-webmvc is on the classpath, Boot automatically registers DispatcherServlet, InternalResourceViewResolver, and a default HandlerMapping. If you have spring-boot-starter-data-jpa, it configures DataSource, EntityManagerFactory, and JpaTransactionManager — all without a single line of XML. The magic is in @ConditionalOnClass, @ConditionalOnMissingBean, and @ConditionalOnProperty annotations. In production, this means your application context can have 200+ beans auto-registered, but you only wrote 10. The downside? When auto-configuration guesses wrong, debugging becomes a forensic exercise — which we'll cover in Section 7.
What the Official Docs Won't Tell You
The official Spring Boot documentation is excellent for getting started, but it glosses over three critical gotchas. First, auto-configuration classes are not singletons — each @Configuration class can be instantiated multiple times if imported by multiple auto-configuration groups. This causes duplicate bean definitions that silently override each other. Second, the spring.factories file is not the only mechanism; Spring Boot 3.x also uses AutoConfiguration.imports files under META-INF/spring/, and if you have both, the behavior is additive but with unpredictable ordering. Third, the @SpringBootApplication annotation is a composite of @Configuration, @EnableAutoConfiguration, and @ComponentScan — but @ComponentScan by default scans only the package of the annotated class and sub-packages. If you have beans in a parent package, they won't be picked up. In a payment-processing migration, we spent three days wondering why a @Service in a parent package wasn't injected — the fix was adding scanBasePackages to @SpringBootApplication. Also, never assume that spring-boot-starter-web includes all Jackson features; you still need jackson-databind for XML support. The docs mention it, but buried in a table.
Dependency Management: Spring BOM vs Spring Boot Starters
In traditional Spring, you manually manage dependency versions in your pom.xml or build.gradle. You need to include spring-webmvc, spring-orm, jackson-databind, hibernate-validator, and ensure they're all compatible. One wrong version can cause ClassNotFoundException or NoSuchMethodError at runtime. Spring Boot introduces the spring-boot-starter-parent POM, which defines a Bill of Materials (BOM) with tested, compatible versions of all Spring projects and common third-party libraries. For example, spring-boot-starter-web pulls in spring-webmvc, spring-web, jackson-databind, hibernate-validator, and embedded Tomcat — all version-managed. The starter pattern is opinionated: you get a production-ready stack with minimal decisions. But there's a catch: if you override a version in your own POM, you're on your own. Spring Boot won't validate compatibility. In a migration, the biggest pain is when you have existing dependencies that conflict with Boot's managed versions. For instance, if your legacy project uses Hibernate 5.6 and Spring Boot 3.x manages Hibernate 6.2, you'll face API changes. The solution is to use the spring-boot-starter-parent as parent, then explicitly override versions via <properties> only when necessary. Also, never mix spring-boot-starter-web and spring-boot-starter-webflux in the same module — they conflict on reactive vs blocking semantics.
Embedded Server vs External Tomcat: Deployment and Configuration
Traditional Spring MVC applications are deployed as WAR files to an external servlet container like Tomcat, Jetty, or WildFly. You configure web.xml with DispatcherServlet, context-param for contextConfigLocation, and possibly a contextLoaderListener. Spring Boot changes this entirely: it embeds the servlet container (Tomcat by default) directly into the application JAR. The main() method calls SpringApplication.run(), which starts the embedded server. This eliminates the need for web.xml — all configuration is now in application.properties or Java config. For example, server.port=8443, server.ssl.key-store=classpath:keystore.p12. The embedded server is configured programmatically via WebServerFactoryCustomizer. In production, this simplifies deployment: you just run java -jar app.jar. However, there are trade-offs. Embedded Tomcat's default settings are optimized for development, not production. You must explicitly tune thread pools, connection timeouts, and access logging. Also, if you need JNDI datasources (common in legacy enterprise apps), embedded Tomcat doesn't support them out of the box — you need to use TomcatJNDI or switch to a connection pool like HikariCP. Another pain point: SSL configuration. In external Tomcat, you configure SSL in server.xml. In Boot, you use server.ssl.* properties, but the keystore must be in the classpath or an absolute path. Many teams forget to handle keystore passwords securely — never hardcode them; use environment variables or Spring Cloud Config.
Configuration Management: XML vs application.properties vs YAML
Legacy Spring applications often have multiple XML configuration files: applicationContext.xml for service layer, dispatcher-servlet.xml for web layer, and dataSource.xml for persistence. These files define beans, property placeholders, and component scans. Spring Boot consolidates all configuration into a single application.properties or application.yml file, plus Java-based @Configuration classes. The key advantage is externalized configuration: you can have application-dev.properties, application-prod.properties, and activate them via spring.profiles.active. Boot also supports relaxed binding: property names can be in camelCase, kebab-case, or underscores — all map to the same Java field. For example, spring.datasource.url, spring.datasource.URL, and spring.datasource.url are all valid. But this flexibility can cause confusion: if you have both spring.datasource.url and spring.datasource.jdbc-url, which one takes precedence? The answer is: it depends on the DataSource implementation. HikariCP uses jdbc-url, but Boot's auto-configuration uses url. Always check the specific property names in the documentation. Another migration pain point: @PropertySource annotations in legacy code. Boot automatically loads application.properties, but if you have custom .properties files, you must still use @PropertySource or configure them via spring.config.import. Also, XML-defined property placeholders like ${db.url} still work if you load them via @Value, but Boot's Environment abstraction is preferred.
Testing: Spring TestContext Framework vs Spring Boot Test Slices
In traditional Spring, testing involves loading the full application context with @ContextConfiguration, specifying XML or Java config locations. This is slow — a typical integration test can take 30 seconds to start the context. Spring Boot introduces test slices: @WebMvcTest, @DataJpaTest, @JdbcTest, etc. These annotations load only the relevant beans for that layer. For example, @WebMvcTest loads only controllers, filters, and converters — not services or repositories. This cuts test startup time to under 5 seconds. Under the hood, each test slice uses a specific auto-configuration class. @WebMvcTest uses WebMvcAutoConfiguration and excludes full component scanning. You can still mock dependencies with @MockBean. For integration tests, @SpringBootTest loads the full context — but you can use @AutoConfigureTestDatabase to replace the real database with an embedded one. In production, we found that @DataJpaTest with an embedded H2 database sometimes passes but fails with real MySQL due to dialect differences. Always use Testcontainers for database tests that match production. Another gotcha: @SpringBootTest with webEnvironment = RANDOM_PORT starts the embedded server, which can conflict with other tests if they don't clean up. Use @DirtiesContext to reset the context between test classes.
Migration Strategy: Step-by-Step from Spring 5.x to Spring Boot 3.x
Migrating a legacy Spring MVC application to Spring Boot requires a phased approach. Phase 1: Dependency audit. Run 'mvn dependency:tree' and identify all Spring and third-party versions. Replace them with spring-boot-starter-parent as parent POM. Remove explicit version declarations for managed dependencies. Phase 2: Configuration migration. Move all XML bean definitions to Java @Configuration classes. Start with DataSource, TransactionManager, and JPA configuration — these are the most critical. Use @ConfigurationProperties to replace property placeholders. Phase 3: Web layer migration. Remove web.xml and replace DispatcherServlet configuration with @SpringBootApplication. Add server.* properties to application.yml. If you have custom servlets or filters, register them as @Bean. Phase 4: Testing migration. Replace @ContextConfiguration with @SpringBootTest or test slices. Add spring-boot-starter-test dependency. Phase 5: Production hardening. Add Actuator, set up health checks, configure logging, and tune embedded server. Throughout the migration, use the auto-configuration report (debug=true) to verify what Boot is configuring. Common pitfalls: forgetting to exclude auto-configuration classes that conflict with your custom beans, and not handling @PropertySource annotations that reference files outside the classpath. Also, if you use Spring Security, migrate from XML security configuration to the filter chain bean. The entire migration for a medium-sized project (50-100 beans) takes about 2-3 weeks, but the payoff is faster development cycles and easier deployment.
Production Debugging: Reading the Auto-Configuration Report
When Spring Boot behaves unexpectedly, the first tool you should reach for is the auto-configuration report. Set debug=true in application.properties and restart the application. The console will print 'Positive matches' (auto-configuration classes that applied), 'Negative matches' (classes that didn't apply, with reasons), and 'Unconditional classes' (always applied). This report is your best friend for diagnosing why a bean wasn't created. For example, if your DataSource isn't configured, the report might show: 'DataSourceAutoConfiguration did not match because @ConditionalOnClass did not find HikariCP'. That tells you HikariCP is missing from classpath. Another powerful feature is the /actuator/conditions endpoint (if Actuator is enabled). It returns the same report as JSON, which you can parse programmatically. In production, we use this endpoint in a health check script that alerts if certain auto-configuration classes are unexpectedly missing. Also, use the /actuator/beans endpoint to list all beans and their dependencies. If a bean is missing, check if its type is in the list. Common issues: a @ConditionalOnProperty expects a property with a specific prefix, but you used a different one. Or @ConditionalOnClass fails because the class is on compile-time classpath but not runtime. Always check the report before diving into code.
The Case of the Vanishing Transaction Manager
- Always verify which TransactionManager auto-configuration picks when multiple data access technologies are on classpath.
- Use debug logging for org.springframework.boot.autoconfigure to see auto-configuration reports.
- Never assume auto-configuration matches your legacy XML — always test transaction commit/rollback behavior.
mvn dependency:tree | grep springcurl -s http://localhost:8080/actuator/beans | jq '.contexts.default.beans'| File | Command / Code | Purpose |
|---|---|---|
| AutoConfigurationDebugExample.java | @Configuration | Core Architecture |
| SpringBootApplicationScanIssue.java | @SpringBootApplication(scanBasePackages = {"com.example.app", "com.example.commo... | What the Official Docs Won't Tell You |
| pom.xml | Dependency Management | |
| application.yml | server: | Embedded Server vs External Tomcat |
| DataSourceConfigMigration.java | @Configuration | Configuration Management |
| PaymentControllerTest.java | @WebMvcTest(PaymentController.class) | Testing |
| SecurityMigration.java | @Configuration | Migration Strategy |
| ActuatorConditionsCheck.java | @WebEndpoint(id = "custom-conditions") | Production Debugging |
Key takeaways
Interview Questions on This Topic
Explain how Spring Boot auto-configuration works under the hood. What is the role of spring.factories?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
That's Spring Boot. Mark it forged?
6 min read · try the examples if you haven't