Skip to content
ExponaBLOG
JAVA, SPRING FRAMEWORK & SPRING BOOT · DAY 021 / 100

Spring vs Spring Boot: what is the difference explained

Spring is the framework. Spring Boot is the opinionated way to run it.

Expona Admin6 min read

Spring vs Spring Boot: what is the difference explained

After fifteen years in production environments, we still see teams conflate Spring and Spring Boot as interchangeable terms. They're not. Understanding the distinction is essential when you're architecting a new service, migrating a legacy monolith, or onboarding junior engineers. Spring is the foundation—a modular, dependency-injection-powered framework. Spring Boot is the opinionated, convention-over-configuration layer that lets you ship production services in minutes instead of days.

Spring: The Foundation of Enterprise Java

Spring started as a dependency injection container and has evolved into a comprehensive ecosystem. At its core, Spring is about decoupling: you define beans, wire them together declaratively, and let the container manage their lifecycle. This inversion of control is what makes large teams productive—components don't know about each other; they depend on abstractions.

When you use Spring directly (without Boot), you're responsible for configuration. Here's what that looks like:

java
// Traditional Spring XML configuration
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userRepository" class="com.example.UserRepository">
        <constructor-arg ref="dataSource"/>
    </bean>

    <bean id="userService" class="com.example.UserService">
        <constructor-arg ref="userRepository"/>
    </bean>

</beans>

Or with Java config, which is more common now:

java
@Configuration
public class ApplicationConfig {

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
        config.setUsername("admin");
        config.setPassword("secret");
        return new HikariDataSource(config);
    }

    @Bean
    public UserRepository userRepository(DataSource dataSource) {
        return new UserRepository(dataSource);
    }

    @Bean
    public UserService userService(UserRepository userRepository) {
        return new UserService(userRepository);
    }
}

Notice the ceremony: you're explicitly wiring every piece. Spring modules handle different concerns:

  • Spring Core & Beans: DI container, lifecycle management
  • Spring MVC: web layer, request routing, view rendering
  • Spring Data: repository abstractions for databases
  • Spring Security: authentication, authorization, session management
  • Spring Integration: message-driven architectures

Each module is optional. You pick what you need and integrate them manually. This flexibility is powerful but demands expertise. We've seen junior developers spend weeks battling classpath issues or discovering that they forgot to enable transaction management because there was no auto-configuration to save them.

Spring Boot: Opinionated Defaults and Convention

Spring Boot changes the equation. Instead of you configuring Spring, Spring Boot configures Spring for you based on what's on your classpath. Add a Postgres driver? Boot assumes you want a connection pool and injects a DataSource. Add Spring Security? Boot sets up a default login form and disables CSRF selectively.

This is auto-configuration. It's a set of conditional bean definitions that activate based on the presence of libraries. The magic happens via the spring-boot-starter-* dependencies:

java
// pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.2.0</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>3.2.0</version>
</dependency>

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.0</version>
</dependency>

One starter pulls in multiple related dependencies—web brings Tomcat, Jackson, Spring MVC. Data JPA brings Hibernate, Spring Data repositories. Postgres is the driver. Boot sees all of these and auto-configures a JPA entity manager, a connection pool, transaction management, and web handlers. Your configuration shrinks dramatically:

java
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

// application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=update

That's it. You have a running Spring application with a web server, database connection, and transaction support. The starter dependencies bundle compatible versions, so you avoid the "wrong version of Spring Data" problem. We've used this in production across trading platforms, e-commerce systems, and ride-hailing services—it consistently reduces setup friction.

Embedded Server and Runnable Artifacts

Spring MVC traditionally deploys to an external servlet container like Tomcat or JBoss. You package your code as a WAR file, drop it into the container's webapps directory, and restart. This separation was useful in 2005; it's overhead today.

Spring Boot embeds the servlet container directly in your JAR. Tomcat (by default), Jetty, or Undertow runs as part of your application process. Your entire service is one executable JAR:

java
// Your REST controller
@RestController
@RequestMapping("/api/users")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }
}

// Run it
// mvn clean package
// java -jar target/app-1.0.0.jar

No container management, no WAR deployment dance. The JAR includes Tomcat, your code, and all dependencies. We've deployed Boot apps to AWS ECS, Lambda, Kubernetes, and bare VMs without repackaging. This portability alone justifies Boot for most teams.

You can also customize the server:

java
// application.properties
server.port=8081
server.servlet.context-path=/api
server.tomcat.max-threads=100
server.compression.enabled=true

Production Features via Spring Boot Actuator

Running code in production means observability. You need metrics, health checks, and insight into what's happening. Spring Boot Actuator provides this out of the box.

Add the dependency:

java
// pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>3.2.0</version>
</dependency>

Suddenly you have endpoints:

  • /actuator/health – application health status, database connectivity, disk space
  • /actuator/metrics – JVM memory, GC, HTTP request latency, custom business metrics
  • /actuator/env – active configuration, property sources
  • /actuator/loggers – runtime log level adjustments
  • /actuator/threaddump – JVM thread state for debugging deadlocks

Kubernetes probes for liveness and readiness map naturally to /actuator/health/live and /actuator/health/ready. We integrate these with Prometheus for metrics collection and Grafana for dashboards. Without Actuator, you'd write all of this custom code.

Define custom health indicators and metrics:

java
@Component
public class ExternalServiceHealthIndicator extends AbstractHealthIndicator {

    private final RestTemplate restTemplate;

    public ExternalServiceHealthIndicator(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    protected void doHealthCheck(Health.Builder builder) {
        try {
            restTemplate.getForObject("https://api.example.com/health", String.class);
            builder.up();
        } catch (Exception e) {
            builder.down().withDetail("reason", e.getMessage());
        }
    }
}

// Inject MeterRegistry for custom metrics
@Service
public class OrderService {
    private final MeterRegistry meterRegistry;

    public OrderService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    public void processOrder(Order order) {
        meterRegistry.counter("orders.processed", "status", "success").increment();
    }
}

Common Mistakes

  • Over-relying on auto-configuration: Boot auto-configures sensibly for most cases, but explicit is sometimes better. Don't assume a bean exists just because you added a starter. Use @ConditionalOnMissingBean in your own configs to override carefully.
  • Ignoring the classpath: Boot's magic depends on what's on your classpath. If you accidentally exclude a transitive dependency, auto-configuration silently doesn't happen. Run mvn dependency:tree to see what's really there.
  • Treating Boot and Spring interchangeably: Boot is a layer on top of Spring. If you're troubleshooting, look for Spring documentation. Boot usually delegates to Spring behavior underneath.
  • Not excluding unwanted auto-configs: Sometimes Boot pulls in a starter you don't want. Use @SpringBootApplication(exclude = {SomeAutoConfiguration.class}) rather than fighting transitive dependencies.
  • Forgetting to externalize configuration: Boot makes it easy to hardcode properties. Externalize everything sensitive or environment-specific to application-{profile}.properties or environment variables for production safety.

Key Takeaways

  • Spring is the modular, dependency-injection framework that powers enterprise Java; Spring Boot is the layer that auto-configures it with sensible defaults.
  • Starter dependencies bundle compatible versions and pull in transitive libraries; they're the fastest path to a working application.
  • Boot embeds a servlet container, making your service a single runnable JAR with no external container needed.
  • Actuator provides production-ready health checks, metrics, and diagnostics; leverage it for Kubernetes readiness probes and monitoring.
  • Use Spring directly when you need fine-grained control; use Boot when you want conventions and rapid development—most teams choose Boot for new services.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles