JavaSpring Boot

Spring Boot vs Spring Framework: What’s the Difference in 2025?

Spring Boot vs Spring Framework: What's the Difference in 2025?

If you’re building Java applications in 2025, you’re choosing between Spring Boot and the Spring Framework. While they’re closely related, they serve different purposes—and understanding these differences can save you significant time and complexity. In this comprehensive guide, we’ll explore both frameworks through practical examples, configuration comparisons, and real-world scenarios to help you make the right choice for your project.

What Is Spring Framework?

The Spring Framework is a comprehensive, modular platform for developing Java applications. Introduced in 2003 by Rod Johnson as an alternative to heavyweight Java EE, it revolutionized enterprise Java development by providing:

  • Dependency Injection (DI) – Inversion of Control container for loose coupling
  • Aspect-Oriented Programming (AOP) – Cross-cutting concerns like logging and security
  • Transaction management – Declarative transaction handling
  • MVC architecture – Web application development
  • Data access – Integration with JDBC, JPA, Hibernate

Spring Framework Configuration Example

Here’s what a typical Spring Framework application looks like with XML configuration:

<!-- applicationContext.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx.xsd">

    <context:component-scan base-package="com.example"/>
    
    <!-- DataSource configuration -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <property name="driverClassName" value="org.postgresql.Driver"/>
        <property name="url" value="jdbc:postgresql://localhost:5432/mydb"/>
        <property name="username" value="user"/>
        <property name="password" value="password"/>
    </bean>

    <!-- EntityManagerFactory -->
    <bean id="entityManagerFactory" 
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="packagesToScan" value="com.example.entity"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="showSql" value="true"/>
                <property name="generateDdl" value="true"/>
            </bean>
        </property>
    </bean>

    <!-- Transaction Manager -->
    <bean id="transactionManager" 
          class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>

    <tx:annotation-driven/>
</beans>

And the Java configuration equivalent:

@Configuration
@EnableTransactionManagement
@ComponentScan("com.example")
public class AppConfig {

    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.postgresql.Driver");
        dataSource.setUrl("jdbc:postgresql://localhost:5432/mydb");
        dataSource.setUsername("user");
        dataSource.setPassword("password");
        return dataSource;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource());
        em.setPackagesToScan("com.example.entity");
        
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setShowSql(true);
        vendorAdapter.setGenerateDdl(true);
        em.setJpaVendorAdapter(vendorAdapter);
        
        return em;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return transactionManager;
    }
}

What Is Spring Boot?

Spring Boot is an opinionated layer on top of Spring Framework that simplifies configuration and deployment. It provides:

  • Auto-configuration – Automatic setup based on classpath dependencies
  • Embedded servers – Tomcat, Jetty, or Netty included
  • Starter dependencies – Curated dependency sets for common use cases
  • Production-ready features – Health checks, metrics, externalized configuration
  • Native image support – GraalVM compilation for fast startup

Spring Boot Equivalent

The same application in Spring Boot:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: user
    password: password
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update

That’s it. Spring Boot auto-configures the DataSource, EntityManagerFactory, and TransactionManager based on the classpath dependencies and properties.

Detailed Comparison

Project Setup

Spring Framework:

<!-- pom.xml - Manual dependency management -->
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.1.4</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>6.1.4</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>6.1.4</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>6.4.4</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.16.1</version>
    </dependency>
    <!-- Many more dependencies with version management -->
</dependencies>

Spring Boot:

<!-- pom.xml - Starter dependencies -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.3</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
</dependencies>

Web Application Configuration

Spring Framework (web.xml required):

<!-- web.xml -->
<web-app>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring/dispatcher-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
</web-app>

Spring Boot (no web.xml needed):

// Just add spring-boot-starter-web dependency
// DispatcherServlet is auto-configured

REST API Controller

The controller code is identical for both:

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public List<Product> getAllProducts() {
        return productService.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        return productService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@Valid @RequestBody ProductDTO dto) {
        Product product = productService.create(dto);
        URI location = URI.create("/api/products/" + product.getId());
        return ResponseEntity.created(location).body(product);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(
            @PathVariable Long id,
            @Valid @RequestBody ProductDTO dto) {
        return productService.update(id, dto)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        productService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

Security Configuration

Spring Framework:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        // Manual configuration required
        UserDetails user = User.withUsername("user")
            .password(passwordEncoder().encode("password"))
            .roles("USER")
            .build();
        return new InMemoryUserDetailsManager(user);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Spring Boot:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }
    
    // UserDetailsService and PasswordEncoder auto-configured
    // if spring.security.user.name and password are set
}
# application.yml - Security auto-configured
spring:
  security:
    user:
      name: admin
      password: secret
      roles: ADMIN

Production Readiness

Spring Boot includes production-ready features out of the box:

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when-authorized
  health:
    db:
      enabled: true
    redis:
      enabled: true

Access these endpoints:

# Health check
curl http://localhost:8080/actuator/health

# Response
{
  "status": "UP",
  "components": {
    "db": { "status": "UP" },
    "diskSpace": { "status": "UP" },
    "redis": { "status": "UP" }
  }
}

# Metrics
curl http://localhost:8080/actuator/metrics/jvm.memory.used

# Prometheus format
curl http://localhost:8080/actuator/prometheus

Native Image Compilation

Spring Boot 3 supports GraalVM native images for faster startup:

# Build native image
./mvnw -Pnative native:compile

# Or with Gradle
./gradlew nativeCompile

# Run native executable
./target/myapp

# Startup time: ~50ms vs ~2s for JVM
<!-- pom.xml native profile -->
<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Comprehensive Comparison Table

Feature Spring Framework Spring Boot
Setup Time Hours to days Minutes
Configuration XML or Java config Auto-configuration + properties
Web Server External (Tomcat, JBoss) Embedded (Tomcat, Jetty, Netty)
Deployment WAR file to server Standalone JAR
Dependency Management Manual version coordination Starter BOMs
Health Checks Custom implementation Actuator built-in
Metrics Manual setup Micrometer auto-configured
Native Images Complex setup First-class support
Docker Support Manual Dockerfile Buildpacks, layered JARs
Testing Manual test context @SpringBootTest slices
Learning Curve Steep Gentle

When to Use Each

Choose Spring Framework When:

  • Fine-grained control – You need complete control over every configuration detail
  • Legacy integration – Working with existing Java EE systems or application servers
  • Complex modularity – Building highly modular applications with custom bootstrapping
  • Specialized deployment – Deploying to traditional application servers (WebSphere, WebLogic)
  • Learning purposes – Understanding how Spring works under the hood

Choose Spring Boot When:

  • New projects – Starting any greenfield application
  • Microservices – Building distributed systems
  • REST APIs – Creating web services
  • Cloud deployment – Targeting Kubernetes, AWS, GCP, Azure
  • Rapid development – MVPs, prototypes, or time-sensitive projects
  • Container deployment – Docker-first deployment strategy

Common Mistakes to Avoid

1. Using Spring Framework When Boot Would Suffice

// Wrong - manual configuration for simple API
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter());
    }
}

// Right - Spring Boot auto-configures Jackson
// Just add spring-boot-starter-web

2. Disabling Auto-Configuration Unnecessarily

// Wrong - disabling useful auto-configuration
@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,
    JpaRepositoriesAutoConfiguration.class
})
public class Application { }

// Right - leverage auto-configuration, customize via properties
spring:
  datasource:
    url: jdbc:h2:mem:testdb

3. Not Using Profiles Effectively

# application-dev.yml
spring:
  datasource:
    url: jdbc:h2:mem:devdb
  jpa:
    show-sql: true

# application-prod.yml
spring:
  datasource:
    url: jdbc:postgresql://prod-db:5432/app
  jpa:
    show-sql: false

4. Ignoring Actuator in Production

# Always configure actuator for production
management:
  endpoints:
    web:
      exposure:
        include: health,info
  endpoint:
    health:
      probes:
        enabled: true  # Kubernetes probes

Final Thoughts

Spring Boot and Spring Framework aren’t competitors—they’re complementary. Spring Boot builds on Spring Framework’s foundation while eliminating boilerplate configuration. For most modern applications in 2025—REST APIs, microservices, cloud-native apps—Spring Boot is the clear choice. It provides faster development, easier deployment, and production-ready features out of the box. Reserve Spring Framework for cases requiring complete configuration control or integration with legacy systems.

To get started with Spring Boot, read Getting Started with Spring Boot 3 and Building Reactive APIs with Spring WebFlux. For official documentation, visit the Spring Boot Reference and the Spring Framework Documentation.

1 Comment

Leave a Comment