Spring Boot has become the default for Java backends. It’s also huge — hundreds of auto-configurations, dozens of starter modules, decades of accumulated concepts. New developers often drown trying to understand all of it. You don’t have to. This article covers what you actually use daily and gives you a framework to learn the rest when you need it.

The core idea

Spring Boot is Spring plus strong defaults. That’s it. Regular Spring requires you to wire everything: beans, datasources, web servers, configuration. Spring Boot says: “if you have spring-boot-starter-web on the classpath, you probably want Tomcat, a JSON mapper, and a web application context. I’ll set it all up. You can override anything.”

The defaults are what makes it fast to start. The override mechanisms are what keep it usable at scale.

What you use every day

@SpringBootApplication

One annotation, three behaviors bundled:

  • @Configuration — this class defines beans
  • @EnableAutoConfiguration — scan classpath, apply matching auto-configurations
  • @ComponentScan — find other @Component-annotated classes in this package and below

It’s the entry point of every Spring Boot app.

Starters

spring-boot-starter-web adds web capabilities. spring-boot-starter-data-jpa adds JPA + Hibernate. spring-boot-starter-kafka, -security, -actuator, etc. Each is a curated group of dependencies that work together.

You almost never pull in raw Spring dependencies. Always use starters.

Configuration

Two files cover 95% of cases:

application.yml — main config:

server:
  port: 8080

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/app
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate

logging:
  level:
    root: INFO
    com.company: DEBUG

application-<profile>.yml — per-environment overrides (application-prod.yml, application-dev.yml). Activate via SPRING_PROFILES_ACTIVE=prod.

@Value and @ConfigurationProperties pull values into code:

@ConfigurationProperties(prefix = "app.payments")
public record PaymentsConfig(
    String baseUrl,
    Duration timeout,
    int retryAttempts
) {}

Typed config beats scattered @Value annotations.

Web layer

@RestController
@RequestMapping("/orders")
public class OrderController {
    private final OrderService service;

    public OrderController(OrderService service) { this.service = service; }

    @PostMapping
    public ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest req) {
        return ResponseEntity.status(CREATED).body(OrderResponse.from(service.place(req)));
    }

    @GetMapping("/{id}")
    public OrderResponse get(@PathVariable UUID id) {
        return OrderResponse.from(service.find(id));
    }
}

That’s 90% of what you write. Add exception handlers with @ControllerAdvice, validation with Jakarta Validation, and you’re done.

Data layer

Spring Data JPA eliminates the boilerplate of writing repositories:

public interface OrderRepository extends JpaRepository<Order, UUID> {
    List<Order> findByCustomerIdAndStatus(UUID customerId, OrderStatus status);
    Optional<Order> findByExternalRef(String ref);
}

For complex queries, use @Query with JPQL or native SQL. For anything really complex, skip JPA and use jOOQ or plain JDBC — JPA at 100M rows becomes painful.

Actuator

spring-boot-starter-actuator exposes operational endpoints:

  • /actuator/health — readiness/liveness
  • /actuator/metrics — Micrometer metrics
  • /actuator/info — build info
  • /actuator/prometheus — Prometheus scrape target

Always include it. Lock it down with security so only internal clients hit it.

What you use occasionally

Profiles and conditional beans

@Bean
@Profile("prod")
DataSource prodDataSource() { ... }

@Bean
@Profile({"dev", "test"})
DataSource devDataSource() { ... }

Useful for environment-specific setups. Don’t overuse — most config belongs in application-<profile>.yml, not in code.

Custom auto-configuration

Useful when you’re building internal libraries that other services consume. Package common configuration once, let services get it for free via a starter. Overkill for application code.

Events

Spring’s ApplicationEventPublisher for in-process pub-sub:

eventPublisher.publishEvent(new OrderPlacedEvent(order));

@EventListener
public void onOrderPlaced(OrderPlacedEvent event) { ... }

Useful for decoupling within a service. Don’t confuse with Kafka-style distributed events.

Caching

@Cacheable, @CacheEvict — work via AOP proxy. Convenient but subtle. For anything complex (TTL per cache, two-level caching), use Caffeine or Redis directly instead.

What you’ll rarely touch

  • BeanPostProcessor / BeanFactoryPostProcessor — for framework internals
  • ApplicationContextInitializer — for custom bootstrap
  • Custom HandlerMethodArgumentResolver — for exotic request parameter types
  • Reactive stack (WebFlux) — only if your app is genuinely I/O-bound and you understand the complexity trade-off
  • AOP with custom aspects — powerful but often a signal of coupling you should fix structurally

Don’t feel bad about not knowing these. The teams that do know them use them sparingly.

Starting a new service — a sensible baseline

spring-boot-starter-web            // REST endpoints
spring-boot-starter-data-jpa       // Postgres + repositories
spring-boot-starter-validation     // @Valid on requests
spring-boot-starter-actuator       // health, metrics
spring-boot-starter-test           // JUnit + Mockito (test scope)
micrometer-registry-prometheus     // metrics export
flyway-core                        // DB migrations
org.testcontainers (postgres, kafka) // integration tests

That’s a production-ready skeleton. Add Kafka, Redis, security as needed.

The mental model

Think of Spring Boot in three layers:

  1. Plain Java classes — your business logic. Would work without Spring.
  2. DI wiring — constructor injection, @Service, @Repository, @Controller. Spring glues the graph.
  3. Auto-configuration — on top of your code, Spring adds a web server, a datasource, metrics, etc. based on what starters you included.

When something misbehaves, unwind in reverse: check auto-config first (often a flag in application.yml), then wiring, then your own code.

What people get wrong

Writing fat services. One @Service doing ten things. Split by responsibility, not by accident.

Skipping tests because of Spring boot time. Slice tests (@WebMvcTest, @DataJpaTest) load only what they need. Pure unit tests load nothing. Only integration tests need the full context.

Annotation soup. Every new field gets an annotation. When it’s not clear what the annotation does, look it up or remove it.

Auto-configuration as magic. When a bean “just appears” and you don’t know why, --debug on startup shows the auto-configuration report. Everything is explicit; you just have to know where to look.

Closing thought

Spring Boot rewards a deliberate approach. The framework is deep; the 20% of features you use daily is enough to build almost any backend. Learn the rest as needs arise. A Spring Boot app written by someone who knows when not to use Spring features tends to outperform and outlive one written by someone who uses everything available.