# Spring Boot From Zero

> Learn Spring Boot the way it's actually used: what auto-configuration really does, dependency injection and beans, building a REST API, configuration and profiles, persistence with Spring Data JPA, the service layer and validation, error handling, testing, security, and shipping to production. Mental-model-first, magic demystified.


---

# Spring Boot From Zero

Spring Boot is the framework most professional Java is written in. If you want a backend job in the Java
world - banks, enterprises, most of the JVM ecosystem - this is the thing they're hiring for. It has a
reputation for "magic": you add an annotation, a dependency appears wired up; you name a method a certain
way and a database query writes itself. That magic is wonderful when it works and baffling when it
doesn't - so this guide's whole job is to show you *what the magic actually is*, not hand you spells to
paste.

We build the mental model first the whole way: what Spring Boot is doing under each annotation, why it
made the choice it did, and how to reason about it when something breaks. By the end you'll be able to
build a real REST API backed by a database - with config, validation, error handling, tests, and
security - and understand every layer instead of cargo-culting it.

> 📝 This guide teaches the **framework**. It assumes you know **Java** - classes, interfaces, generics,
> annotations, exceptions. If you don't yet, do [Java From Zero](/guides/java-from-zero) first; a framework
> amplifies the language under it, it can't replace it. New to frameworks as a concept? Read
> [What a Framework Even Is](/guides/what-a-framework-even-is) for the mental model this builds on.

## How to read this

Read in order - each phase builds the running example (a small REST API) one layer at a time. Type the
code as you go; a Spring app clicks once you've wired the pieces yourself. Phases carry difficulty badges
so you can see the climb.

## The phases

**Part 1 - The core (🟢 Basic)**
1. **[What Spring Boot Is & Your First App](01-what-spring-boot-is.md)** 🟢 - Spring vs Spring Boot, auto-configuration demystified, Spring Initializr, your first running web app.
2. **[Dependency Injection & Beans](02-dependency-injection-and-beans.md)** 🟢 - the heart of Spring: the IoC container, beans, `@Component`/`@Service`, constructor injection.
3. **[Building a REST API: Controllers](03-rest-controllers.md)** 🟢 - `@RestController`, request mappings, path variables, request bodies, returning JSON.

**Part 2 - A real application (🟡 Intermediate)**
4. **[Configuration & Profiles](04-configuration-and-profiles.md)** 🟡 - `application.yml`, `@Value`, `@ConfigurationProperties`, and per-environment profiles.
5. **[Persistence with Spring Data JPA](05-persistence-with-jpa.md)** 🟡 - `@Entity`, repositories, CRUD without SQL, derived queries, and where the magic stops.
6. **[The Service Layer, DTOs & Validation](06-service-layer-and-validation.md)** 🟡 - separating concerns, DTOs vs entities, Bean Validation, `@Transactional`.
7. **[Error Handling Done Right](07-error-handling.md)** 🟡 - `@ExceptionHandler`, `@ControllerAdvice`, and correct HTTP status codes.
8. **[Testing Spring Boot Apps](08-testing-spring-boot.md)** 🟡 - `@SpringBootTest`, slice tests, MockMvc, and testing the data layer.

**Part 3 - Production (🔴 Advanced → 🟡)**
9. **[Security with Spring Security](09-security-with-spring-security.md)** 🔴 - the filter chain, authentication vs authorization, password encoding, securing endpoints.
10. **[Production: Actuator, Packaging & Deployment](10-production-actuator-and-deploy.md)** 🟡 - health/metrics with Actuator, building a runnable JAR, Docker, prod profiles.

**Finale**
11. **[Where to Go Next](11-where-to-go-next.md)** 🟢 - microservices, reactive (WebFlux), messaging, and what to build.

> The "magic" of Spring Boot is auto-configured Spring. When you want to see what it automates by hand,
> the [Spring Framework (core)](/guides/spring-framework-from-zero) guide (writing the config yourself) is
> the demystifier - but learn it *after* this; Boot is how the job is actually done.


---

# What Spring Boot Is & Your First App

If you've heard Spring described as "the heavyweight enterprise Java thing with a thousand XML files," that reputation was earned - about fifteen years ago. Spring Boot is the answer the Spring team built to that exact pain. The single biggest thing that confuses newcomers isn't the syntax - it's not understanding what the framework is doing on your behalf. Once you can see the machinery, Boot stops feeling like magic and starts feeling like a very well-organized assistant.

We'll untangle Spring from Spring Boot, demystify "auto-configuration," meet starters, and stand up a real web app you can hit in a browser.

This guide assumes you're comfortable with Java classes, methods, and annotations - if `public class`, `@Override`, and `new` aren't second nature yet, spend a little time in [/guides/java-from-zero](/guides/java-from-zero) first. It also builds on the framework mental model from [/guides/what-a-framework-even-is](/guides/what-a-framework-even-is) - we'll lean on *inversion of control* more than once.

## The mental model: Spring is the toolbox, Boot is the toolbox pre-assembled

📝 **Spring (the Framework)** is a huge collection of tools for building Java applications: an **IoC container** that constructs and connects your objects, **Spring MVC** for handling web requests, **Spring Data** for talking to databases, **Spring Security** for auth, and a lot more. It's powerful and flexible - and historically, that flexibility meant *you* had to configure every piece by hand, often in long XML files, before anything would run.

📝 **Spring Boot** is Spring with three things added on top: **auto-configuration** (Boot wires up sane defaults for you), **sensible defaults** (it picks reasonable settings so you don't have to specify everything), and an **embedded server** (a web server lives *inside* your app, so there's nothing separate to install and deploy to). The one-line version:

> 💡 **Key point.** Boot doesn't replace Spring - it *is* Spring, with the boilerplate already filled in. Think of Spring as a kitchen full of professional equipment, and Spring Boot as that same kitchen with the oven preheated, the knives sharpened, and a recipe on the counter. Same tools. You just start cooking instead of assembling.

So whenever someone says "I'm using Spring Boot," they're using Spring. Boot is the on-ramp.

## Auto-configuration, demystified

Auto-configuration is the part that feels like sorcery - the single most important idea in this guide.

📝 **Auto-configuration** is Boot looking at what's on your project's **classpath** - the set of libraries your app has available - and configuring those libraries with reasonable defaults, *automatically*, at startup. Add the library that does web stuff, and Boot notices and sets up a web server and JSON handling. Add a database driver, and Boot notices and sets up a connection to the database.

The crucial thing to internalize:

> 💡 **Key point.** Auto-configuration is *conditional* configuration, not guesswork. Under the hood it's a pile of rules shaped like "**if** the H2 database library is present **and** the developer hasn't already defined a database connection, **then** configure an in-memory H2 database." Boot ships hundreds of these `@Conditional` rules. Each one only fires when its conditions are met. Nothing happens by spooky action - every default is a rule you could read.

That conditional design is also why auto-configuration never traps you. Every rule includes a condition like "...and the developer hasn't configured this themselves." The moment *you* define a piece of configuration, Boot's rule for that piece backs off and lets yours win.

⚠️ **Gotcha - "magic" you can't see is still magic *to you*.** The convenience is real, but a default you didn't choose is a default you might not know about. When something behaves unexpectedly (a port, a JSON format, a database that "appeared"), the cause is almost always an auto-configuration default. Learning to *ask* "what did Boot auto-configure here?" is a core Spring Boot skill - and Boot can print exactly what it decided. We'll come back to that.

The trigger for all this convenience is wonderfully simple: you add a *starter*.

## Starters: curated bundles for one job

You don't hand-pick fifteen libraries and pray their versions are compatible. You add one starter.

📝 A **starter** is a curated bundle of dependencies for a single job, published by the Spring team with versions that are known to work together. `spring-boot-starter-web` is the bundle for building web apps - pull it in and you get Spring MVC, an embedded **Tomcat** server, JSON support (via Jackson), and validation, all in one line. There are starters for data access, security, testing, messaging, and dozens more.

Here's what adding the web starter looks like in a Maven `pom.xml`:

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
```

*What just happened:* you declared a single dependency, and behind it sits an entire stack - the web framework, the server, the JSON serializer, the validator - with mutually compatible versions chosen for you. You did **not** list Tomcat, Jackson, or Spring MVC individually, and you did **not** pin any version numbers (Boot's parent project manages those). One line in, and there's enough on the classpath for auto-configuration to stand up a working web server.

💡 **Insight.** This is the loop that *is* Spring Boot: **you add a starter → that starter puts libraries on the classpath → auto-configuration sees them and wires sane defaults.** Almost everything you'll do in Boot is some version of "add the right starter, let Boot configure it, override the bits you care about."

## Your first app

The fastest way to start a Boot project is **Spring Initializr**.

📝 **Spring Initializr** (at [start.spring.io](https://start.spring.io)) is a web page that generates a ready-to-run Boot project for you. You pick your build tool (Maven or Gradle), your Java version, and which starters you want, then download a zip with the folder structure, build file, and a main class already in place. It's the official "new project" button for Spring Boot.

Go to start.spring.io, choose **Maven**, add the **Spring Web** dependency (that's `spring-boot-starter-web`), and generate. Unzip it, and the heart of what you get is one small class:

```java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
```

*What just happened:* this is the entry point of your whole application. Your normal Java `main` method is still here - Boot didn't invent a new way to start a program - but instead of your code doing the work, it calls `SpringApplication.run(...)` and hands control to Spring. That one call boots the framework: it creates the IoC container, runs auto-configuration, finds your code, and starts the embedded server. This is *inversion of control* in the flesh - `main` is the last moment your code is in charge. After `run(...)`, the framework drives and calls back into your code when it needs you.

The one annotation doing the heavy lifting is `@SpringBootApplication`. It looks innocent, but it's three annotations bundled into one:

📝 **`@SpringBootApplication`** combines:
- **`@SpringBootConfiguration`** - marks this class as a source of bean/config definitions.
- **`@EnableAutoConfiguration`** - turns on the auto-configuration machinery we just discussed.
- **`@ComponentScan`** - tells Spring to scan *this package and everything below it* for your components (controllers, services, and so on) and register them automatically.

⚠️ **Gotcha - package placement matters.** Because `@ComponentScan` searches downward from the package of your main class, anything you write needs to live in that package or a sub-package. A controller dropped into a sibling or parent package won't be found, and you'll get a confusing 404 with no obvious cause. Keep your code under the main class's package and this never bites you.

Now let's add something to actually respond to a request. Create a class next to `DemoApplication`:

```java
package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/")
    public String hello() {
        return "Hello from Spring Boot!";
    }
}
```

*What just happened:* `@RestController` tells Spring "this class holds web request handlers, and whatever they return is the HTTP response body." `@GetMapping("/")` maps HTTP `GET` requests for the root URL to the `hello()` method. You didn't write any code to open a socket, parse the incoming request, match the URL, or format the response - you described *which URL runs which method*, and the framework owns everything around it.

Run it from the project folder. Boot projects ship with a wrapper script so you don't even need Maven installed globally:

```bash
./mvnw spring-boot:run
```

You'll see Boot start up and announce the embedded server:

```console
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.x.x)

INFO  Starting DemoApplication using Java 21
INFO  Tomcat initialized with port 8080 (http)
INFO  Tomcat started on port 8080 (http) with context path '/'
INFO  Started DemoApplication in 1.42 seconds
```

*What just happened:* that output is a receipt of everything Boot did. It started **Tomcat** - a full web server - *inside* your process, on port 8080, without you installing or configuring a server anywhere. Open a browser to `http://localhost:8080` and you'll see `Hello from Spring Boot!` - a real HTTP server, serving your code, from a project you generated minutes ago.

## What you didn't have to do

Count what you *didn't* write, because the gap is the entire value proposition:

- **No server install or setup.** Tomcat came embedded via the web starter and started itself. There's no separate server to download, configure, or deploy a `.war` file into.
- **No JSON wiring.** Return an object from a controller and Boot serializes it to JSON automatically (Jackson, auto-configured). You'll lean on this constantly.
- **No manual object wiring - yet.** The IoC container built and connected the objects in your app. We've only scratched this; it's the whole of [Phase 2: Dependency Injection & Beans](02-dependency-injection-and-beans.md).
- **No version juggling.** The starter and Boot's parent picked compatible library versions, sidestepping the "dependency hell" that plagued old-school Spring.
- **No boilerplate config files.** No XML, no manual server descriptors. Sensible defaults covered it.

Here's the whole loop as one picture:

```mermaid
flowchart LR
  A[Add spring-boot-starter-web] --> B[Libraries land on classpath]
  B --> C[Auto-configuration fires]
  C --> D[Embedded Tomcat + MVC + JSON wired]
  D --> E[Running app on :8080]
```

That convenience is the payoff - and the thing to stay aware of. Every box in that diagram is a place Boot made a decision *for* you. Most of the time those decisions are exactly right, which is why Boot is a joy to start with. But engineers who are great with Spring Boot can name what's in each box when they need to: which server, which defaults, which auto-configuration rules fired.

💡 **Insight - make the magic visible.** Boot can tell you exactly what it auto-configured. Set the property `debug=true` (in `application.properties`) and on startup it prints an **auto-configuration report** listing every rule that matched and every rule that didn't, with the reason. When something behaves in a way you didn't ask for, that report is where "magic" turns into "oh, *that's* why."

## Recap

- **Spring is the toolbox; Spring Boot is that toolbox pre-assembled** - Boot is Spring plus auto-configuration, sensible defaults, and an embedded server. It doesn't replace Spring; it removes the boilerplate.
- **Auto-configuration is conditional configuration, not magic.** Boot reads your classpath and applies hundreds of "if this library is present and you haven't configured it yourself, then set up this default" rules. The moment you configure something, Boot's default steps aside.
- **Starters are curated dependency bundles for one job.** `spring-boot-starter-web` brings Spring MVC, embedded Tomcat, and JSON support in a single line with versions chosen for you.
- **The core loop:** add a starter → libraries hit the classpath → auto-configuration wires sane defaults you can override.
- **`@SpringBootApplication` = config + `@EnableAutoConfiguration` + `@ComponentScan`,** and `SpringApplication.run(...)` is where your code hands control to the framework - inversion of control, live.
- **Stay aware of the defaults.** The convenience is real, but `debug=true` prints the auto-configuration report so the "magic" is always something you can read.

## Quick check

Test the mental model before moving on:

```quiz
[
  {
    "q": "What is the key difference between Spring and Spring Boot?",
    "choices": [
      "Spring Boot is a different framework that replaces Spring entirely",
      "Spring Boot is Spring with auto-configuration, sensible defaults, and an embedded server added on top",
      "Spring is for web apps and Spring Boot is for desktop apps",
      "Spring Boot removes the IoC container that Spring uses"
    ],
    "answer": 1,
    "explain": "Spring Boot doesn't replace Spring - it IS Spring, with the boilerplate pre-filled via auto-configuration, sensible defaults, and an embedded server."
  },
  {
    "q": "How does auto-configuration decide what to set up?",
    "choices": [
      "It asks you a series of questions when the app starts",
      "It configures every possible feature whether you use it or not",
      "It looks at what libraries are on the classpath and applies conditional rules, backing off when you've configured something yourself",
      "It reads a mandatory XML file you must write by hand"
    ],
    "answer": 2,
    "explain": "Auto-configuration is conditional: 'if this library is present and you haven't configured it yourself, then apply this default.' Your own config always wins."
  },
  {
    "q": "What three annotations does @SpringBootApplication bundle together?",
    "choices": [
      "@RestController, @GetMapping, and @Service",
      "@Configuration, @EnableAutoConfiguration, and @ComponentScan",
      "@Autowired, @Bean, and @Component",
      "@SpringBootTest, @Profile, and @Value"
    ],
    "answer": 1,
    "explain": "@SpringBootApplication combines @SpringBootConfiguration (a form of @Configuration), @EnableAutoConfiguration (turns on auto-config), and @ComponentScan (finds your components)."
  }
]
```


---

# Dependency Injection & Beans

In Phase 1 you got a Spring Boot app running and saw that the "magic" was really auto-configured Spring
quietly wiring things up for you. This phase is about the single mechanism doing most of that wiring - 
the one idea that, once it clicks, makes the rest of Spring stop feeling like sorcery: **dependency
injection**, sitting at the dead center of everything Spring does.

The mental model to carry through: in a normal program, your objects build the other objects they need.
In a Spring program, **a container builds your objects for you and hands them the things they depend
on.** You stop writing `new`, and you start *describing* what you need. Get that single inversion, and
`@Service`, `@Autowired`, repositories, controllers - all of it - turn from incantations into plain
machinery.

## The problem: objects building their own dependencies

Say you're writing an `OrderService` that needs to send a confirmation email. The straightforward way:
have the service create the emailer it needs.

```java
public class OrderService {
    private final EmailSender emailSender;

    public OrderService() {
        this.emailSender = new SmtpEmailSender("smtp.acme.com", 587);  // builds its own dependency
    }

    public void placeOrder(Order order) {
        // ... save the order ...
        emailSender.send(order.customerEmail(), "Your order is confirmed!");
    }
}
```
*What just happened:* `OrderService` reached out and built its own `SmtpEmailSender`, hard-coding the
server and port right into its constructor. It works - until it doesn't. Look at what you've trapped
yourself into:

- **Tight coupling.** `OrderService` is now welded to `SmtpEmailSender` specifically. Want to swap in a
  different sender - say one that uses a third-party API? You have to crack open `OrderService` and edit
  it.
- **Untestable.** To test `placeOrder`, you'd fire a *real email* at a *real SMTP server* every test
  run. There's no seam to slip a fake emailer in.
- **Hidden setup.** That SMTP host and port are buried inside the class. Configuration is tangled up with
  business logic.

The root cause: `OrderService` is doing two jobs - deciding *what* its dependency is, and using it.
Dependency injection's whole pitch is to take the first job away.

## Inversion of Control: let the container build your objects

📝 **Inversion of Control (IoC)** - instead of *your code* creating the objects it needs, a **container**
creates them and supplies them to your code. Control over object creation is *inverted*: it moves out of
your classes and into the framework. (This is the framework principle from
[What a Framework Even Is](/guides/what-a-framework-even-is) - "don't call us, we'll call you" - made
concrete: the framework even *builds* your code's objects.)

Spring's container is the engine that does this. You hand it the recipe for your objects; it
constructs them, figures out what each one depends on, and wires the whole graph together at startup.

📝 **Bean** - an object that the Spring container creates, wires up, and manages. That's the entire
definition. A "bean" isn't a special kind of class or some exotic type; it's just one of *your* ordinary
objects that you've handed to Spring to own. When people say "register a bean," they mean "tell the
container to manage this object for you."

So the shift is: you stop writing `new SmtpEmailSender(...)`, and instead you tell Spring "this is a
thing you should manage" and "this service needs one of those." Spring does the connecting. Let's see how
you say that.

## Declaring beans: `@Component` and its flavors

You tell the container "manage this class" by annotating it. The base annotation is `@Component`. When
your app starts, Spring does **component scanning** - it walks the packages under your main application
class, finds every class marked as a component, and creates one instance of each as a bean.

```java
import org.springframework.stereotype.Component;

@Component
public class SmtpEmailSender implements EmailSender {
    @Override
    public void send(String to, String message) {
        System.out.println("Sending email to " + to + ": " + message);
    }
}
```
*What just happened:* `@Component` is a flag that says "Spring, this is yours - make a bean out of it."
On startup, component scanning spots it, calls `new SmtpEmailSender()` *for* you, and stashes the
resulting object in the container, ready to hand to anything that needs an `EmailSender`.

💡 Spring gives you **semantic flavors** of `@Component` that mean the exact same thing to the container
but document a class's *role* - and let Spring add layer-specific behavior later:

- `@Service` - business logic (your `OrderService`).
- `@Repository` - data access; also translates database exceptions into Spring's consistent ones.
- `@Controller` / `@RestController` - handles web requests (Phase 3's whole topic).
- `@Component` - the generic fallback when none of the above fit.

They're all components under the hood. Use the specific one that names what the class *does*; it makes
the architecture readable at a glance. So our service becomes:

```java
import org.springframework.stereotype.Service;

@Service
public class OrderService {
    // ... we'll fill in the dependency next ...
}
```
*What just happened:* `@Service` registers `OrderService` as a bean *and* signals "this is a business-logic
class." Functionally identical to `@Component`; clearer to every human who reads it.

## Injecting dependencies: constructor injection

Now the payoff. `OrderService` needs an `EmailSender`. Instead of building one, it **declares** that it
needs one - by taking it as a constructor parameter. Spring sees the parameter and supplies the matching
bean automatically.

```java
import org.springframework.stereotype.Service;

@Service
public class OrderService {
    private final EmailSender emailSender;   // a dependency, not built here

    public OrderService(EmailSender emailSender) {   // Spring passes one in
        this.emailSender = emailSender;
    }

    public void placeOrder(Order order) {
        // ... save the order ...
        emailSender.send(order.customerEmail(), "Your order is confirmed!");
        System.out.println("Order placed for " + order.customerEmail());
    }
}
```
*What just happened:* `OrderService` no longer knows or cares *which* `EmailSender` it gets - it just asks
for one in its constructor. At startup Spring sees this class needs an `EmailSender`, finds the
`SmtpEmailSender` bean it already created, and passes it in. This is **constructor injection**:
dependencies arrive through the constructor, and the container fills them in. Notice `emailSender` is
`final` - once Spring sets it, it can never change.

This is the recommended way to inject in Spring, and it's worth knowing *why*:

- **Explicit dependencies.** The constructor signature is a clear list of everything this class needs.
  Read the constructor, know the dependencies. Nothing hidden.
- **Immutable & safe.** `final` fields can't be reassigned, and the object is fully built the moment it
  exists - there's no half-constructed window where a dependency is still `null`.
- **Trivially testable.** In a test you just call `new OrderService(fakeEmailSender)` - no Spring required.
  That seam we were missing earlier? The constructor *is* the seam.

💡 When a class has exactly **one constructor**, you don't even need an annotation on it - Spring uses it
for injection automatically. (Older code you'll meet puts `@Autowired` on the constructor; with a single
constructor it's optional and usually omitted now.)

### The discouraged way: field `@Autowired`

You'll see a lot of older tutorials inject straight into a field instead:

```java
@Service
public class OrderService {
    @Autowired                       // ⚠️ field injection - avoid this
    private EmailSender emailSender;

    public void placeOrder(Order order) {
        emailSender.send(order.customerEmail(), "Your order is confirmed!");
    }
}
```
*What just happened:* `@Autowired` on the field tells Spring to reach in and set `emailSender` directly,
no constructor needed. It looks shorter - that's the trap.

⚠️ **Prefer constructor injection over field `@Autowired`.** Field injection hides dependencies (the
constructor no longer lists them, so the only way to know what a class needs is to scan for annotations),
can't use `final` (the field is mutable and `null` until Spring fills it), and is painful to test - you
can't just `new` the object with fakes; you need reflection or a full Spring context. Use the constructor.
Newer code rarely uses field injection at all.

## The ApplicationContext: where swapping implementations pays off

📝 **ApplicationContext** - Spring's container itself; the registry that holds every bean and knows how
they connect. At startup Spring builds the context, fills it with your scanned beans, and wires each one's
dependencies. When your app needs an object, it comes from the context, fully assembled.

Here's the part that makes the whole exercise worth it. `OrderService` depends on the **interface**
`EmailSender`, not on the concrete `SmtpEmailSender`. So the container can inject *any* class that
implements `EmailSender` - and `OrderService` never notices the difference. In production it gets the
real SMTP sender; in a test you swap in a fake:

```java
public class OrderServiceTest {
    @Test
    void placingAnOrderSendsConfirmation() {
        var fakeSender = new RecordingEmailSender();      // a test double, no real email
        var service = new OrderService(fakeSender);       // inject it by hand - that's the seam

        service.placeOrder(new Order("ada@example.com"));

        assertEquals("ada@example.com", fakeSender.lastRecipient());
    }
}
```
```console
$ ./mvnw test
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
```
*What just happened:* because `OrderService` only ever asked for an `EmailSender` interface, the test
handed it a `RecordingEmailSender` instead of the real one - no SMTP server, no network, instant test.
Depend on interfaces, inject through the constructor, and swapping real for fake is one line.

Here's the container's job in one picture - it scans your classes, creates a bean for each, and wires
the dependencies between them:

```mermaid
flowchart TD
  Scan["Component scan finds<br/>@Service / @Component"]
  Ctx["ApplicationContext<br/>(the container)"]
  Sender["EmailSender bean<br/>(SmtpEmailSender)"]
  Order["OrderService bean"]
  Scan --> Ctx
  Ctx -->|creates| Sender
  Ctx -->|creates| Order
  Sender -->|injected into| Order
```

💡 **You describe what you need; the container assembles it.** That's the whole philosophy. You write
classes that *ask* for their dependencies as interfaces, you flag them so Spring manages them, and the
ApplicationContext figures out the full object graph and builds it at startup. Once this is second
nature, every Spring feature you meet from here on is just more beans being wired into that same graph.

## Recap

1. **The problem DI solves:** when a class builds its own dependencies with `new`, it becomes tightly
   coupled, hard to test, and tangled with configuration. DI removes the job of *choosing* dependencies
   from your classes.
2. **Inversion of Control:** the Spring **container** creates your objects and supplies their
   dependencies, instead of you `new`-ing them - the framework principle made concrete. A **bean** is
   an object the container creates, wires, and manages.
3. **Declaring beans:** annotate a class with `@Component` (or the role-specific `@Service`,
   `@Repository`, `@Controller`) and component scanning registers it as a bean at startup.
4. **Constructor injection** is the recommended way: dependencies arrive as constructor parameters, so
   they're explicit, can be `final` (immutable), and trivially testable. ⚠️ Field `@Autowired` is
   discouraged - it hides dependencies and resists testing.
5. **The ApplicationContext** holds every bean. Because you depend on **interfaces**, the container can
   inject any implementation - the real one in production, a fake in tests. You describe what you need;
   the container assembles it.

With dependency injection in hand, you have the spine of every Spring app. Next we put a web layer on
front of these beans and start serving real HTTP requests.

## Quick check

Test yourself on the ideas that have to stick from this phase:

```quiz
[
  {
    "q": "What is a 'bean' in Spring?",
    "choices": [
      "An object that the Spring container creates, wires up, and manages",
      "A special Spring-only base class your classes must extend",
      "A configuration file that lists your dependencies",
      "Any class annotated with @Bean and nothing else"
    ],
    "answer": 0,
    "explain": "A bean is just one of your ordinary objects that you've handed to the container to own. The container creates it, injects its dependencies, and manages its lifecycle. There's no special base class or type involved."
  },
  {
    "q": "Why is constructor injection preferred over field @Autowired?",
    "choices": [
      "Dependencies are explicit in the constructor, fields can be final/immutable, and the class is trivially testable by passing fakes to the constructor",
      "It's faster at runtime because Spring skips reflection",
      "Field injection doesn't work in Spring Boot at all",
      "Constructor injection is the only way to inject interfaces"
    ],
    "answer": 0,
    "explain": "Constructor injection makes dependencies a clear, visible list, allows final fields (immutable, never null), and lets you test with `new MyService(fake)` without any Spring context. Field @Autowired hides dependencies and resists plain testing."
  },
  {
    "q": "OrderService takes an `EmailSender` interface in its constructor. How can a test run it without sending real email?",
    "choices": [
      "Construct OrderService directly and pass a fake EmailSender implementation - the container isn't needed",
      "You can't; you must start the full ApplicationContext and a real SMTP server",
      "Annotate the test with @NoEmail to disable sending",
      "Edit OrderService to remove the EmailSender before testing"
    ],
    "answer": 0,
    "explain": "Because OrderService depends on the EmailSender interface and receives it through the constructor, a test just calls `new OrderService(fakeSender)` with a test double. Depending on interfaces plus constructor injection is exactly what makes implementations swappable."
  }
]
```


---

# Building a REST API: Controllers

In [Phase 2](02-dependency-injection-and-beans.md) you learned how Spring builds and wires your objects - 
the container, beans, dependency injection. Those objects have just sat there, fully wired but waiting.
This phase is where they finally do something visible: respond to an HTTP request.

**A controller is the doorway between the outside world and your code.** HTTP requests arrive from
browsers, mobile apps, and other services, and something has to catch each one, figure out what it's
asking for, run the right code, and send a reply - that "something" is a controller. You don't write the
part that listens on a socket, parses HTTP, or formats response bytes - Spring does that. You write small
methods and *label* them so Spring knows "when a `GET /api/books` comes in, call this one." Same
inversion of control as Phase 2: you don't call Spring, Spring calls you.

The running example for this whole guide is a tiny **book API** - a service that lets clients list, fetch,
and add books. A book is just four fields:

```java
public class Book {
    private Long id;
    private String title;
    private String author;
    private String isbn;
    // constructor, getters, and setters omitted for brevity
}
```

*What just happened:* That's the entity we'll move around for the rest of the phase - an `id`, a `title`, an
`author`, and an `isbn`. Nothing Spring-specific about it yet; it's a plain Java object. (If terms like
*HTTP method*, *status code*, or *JSON body* feel fuzzy, the [REST APIs Explained](/guides/rest-apis-explained)
and [HTTP & JSON API Basics](/guides/http-and-json-api-basics) guides cover the protocol side; here we focus
on the Spring side.)

## `@RestController` and request mapping

📝 A **controller** is a class whose methods handle incoming requests. You mark the class with an annotation,
and Spring registers each handler method against a URL and an HTTP method. When a matching request arrives,
Spring calls your method and turns whatever you return into the HTTP response.

For a JSON API, the annotation you want is `@RestController`. It's actually two annotations rolled into one:
`@Controller` (this class handles web requests) **plus** `@ResponseBody` (whatever a method returns *is* the
response body, serialized to JSON - not the name of an HTML page to render). That `@ResponseBody` part is the
whole reason `@RestController` exists: it says "I'm building an API, not a website."

```java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
public class BookController {

    @GetMapping("/api/books")
    public List<Book> listBooks() {
        return List.of(
            new Book(1L, "The Pragmatic Programmer", "Hunt & Thomas", "9780201616224"),
            new Book(2L, "Clean Code", "Robert C. Martin", "9780132350884")
        );
    }
}
```

*What just happened:* `@RestController` told Spring this class catches HTTP requests and returns response
bodies directly. `@GetMapping("/api/books")` mapped this method to `GET /api/books`. When that request
arrives, Spring calls `listBooks()`, gets back a `List<Book>`, and hands it to **Jackson**, the JSON
library Spring Boot includes by default. Jackson reads each book's fields and produces JSON automatically
 - you never wrote a line of serialization code.

A request and the response it produces:

```http
GET /api/books HTTP/1.1
Host: localhost:8080
```

```json
[
  { "id": 1, "title": "The Pragmatic Programmer", "author": "Hunt & Thomas", "isbn": "9780201616224" },
  { "id": 2, "title": "Clean Code", "author": "Robert C. Martin", "isbn": "9780132350884" }
]
```

*What just happened:* The list of `Book` objects came back as a JSON array, one object per book, each field
mapped by name. That field-by-field translation is Jackson doing its job - your method just returned plain
Java objects.

💡 If you find yourself repeating `/api/books` at the start of every mapping, you can hoist it to the class with
`@RequestMapping("/api/books")` and then write `@GetMapping`, `@GetMapping("/{id}")`, etc. on the methods. Same
result, less repetition. We'll keep the full paths on each method here so every example reads on its own.

## Path variables and request params

A real API needs to address *one specific book* and to *filter* a list. Those are two different jobs, and
Spring has a different tool for each.

📝 A **path variable** is part of the URL path itself - `/api/books/2` means "the book whose id is 2." You write a
placeholder in the mapping with curly braces (`/api/books/{id}`) and bind it to a method parameter with
`@PathVariable`. A **request param** is a query-string value after the `?` - `/api/books?author=Martin` - and you
bind it with `@RequestParam`. The rule of thumb: a path variable *identifies a resource*; a request param
*modifies or filters* a request.

```java
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
public class BookController {

    @GetMapping("/api/books/{id}")
    public Book getBook(@PathVariable Long id) {
        return findById(id);   // look the book up (a real lookup arrives in Phase 5)
    }

    @GetMapping("/api/books")
    public List<Book> listBooks(@RequestParam(required = false) String author) {
        if (author == null) {
            return findAll();
        }
        return findByAuthor(author);   // filter when ?author=... is present
    }
}
```

*What just happened:* In `getBook`, the `{id}` in the path lines up with the `@PathVariable Long id`
parameter - Spring pulls the `2` out of `/api/books/2`, converts the text to a `Long` for you, and passes it in.
In `listBooks`, `@RequestParam(required = false) String author` reads the `?author=...` query string;
`required = false` means the param is optional, so `author` is `null` when the client omits it, and we return
the full list. One method, two behaviors, driven by the query string.

Try both with curl:

```bash
curl http://localhost:8080/api/books/2
curl "http://localhost:8080/api/books?author=Robert%20C.%20Martin"
```

```console
{"id":2,"title":"Clean Code","author":"Robert C. Martin","isbn":"9780132350884"}

[{"id":2,"title":"Clean Code","author":"Robert C. Martin","isbn":"9780132350884"}]
```

*What just happened:* The first call hit the path-variable route and returned a single book object. The
second call hit the same `/api/books` endpoint but, because `?author=` was present, returned a filtered array. The
quotes around the second URL keep the shell from choking on the `?` and the space (encoded as `%20`).

## Request bodies (POST)

Reading is half an API. To *create* a book, the client sends data in the request body, and your method needs
to receive it.

📝 `@PostMapping` maps a method to `POST`, the HTTP method for "create this." The new book arrives as JSON in
the request body, and `@RequestBody` is the annotation that says "take that JSON and turn it into a Java
object for me." Jackson runs in reverse here: it reads the incoming JSON and constructs a `Book`, matching
JSON keys to fields by name.

```java
import org.springframework.web.bind.annotation.*;

@RestController
public class BookController {

    @PostMapping("/api/books")
    public Book createBook(@RequestBody Book book) {
        Book saved = save(book);   // persist it (real persistence comes in Phase 5)
        return saved;              // echo the created book back to the client
    }
}
```

*What just happened:* `@PostMapping("/api/books")` routed `POST /api/books` to this method. `@RequestBody Book book`
told Spring to deserialize the JSON body into a `Book` - Jackson read the keys and filled in the fields. We
"save" it (a placeholder for now) and return the saved book, which Spring serializes straight back to JSON.
The client sends a book and gets the stored version back, typically now carrying its assigned `id`.

The request the client sends:

```json
{
  "title": "Domain-Driven Design",
  "author": "Eric Evans",
  "isbn": "9780321125217"
}
```

*What just happened:* The client posts a book with no `id` - the server assigns that. Jackson binds `title`,
`author`, and `isbn` onto the `Book` object before your method body ever runs, so by the time `createBook`
executes, `book` is a fully populated Java object you can work with.

## `ResponseEntity` and status codes

Every example so far has quietly returned **200 OK**, because that's what Spring does when you return a plain
object. But "200" isn't always the correct answer. Creating a resource should report **201 Created**; asking
for a book that doesn't exist should report **404 Not Found**. To control the status code (and headers), you
return a `ResponseEntity` instead of the bare object.

📝 A **`ResponseEntity<T>`** is a wrapper around your response body that *also* carries the status code and
headers. Return `book` and you get a default 200; return `ResponseEntity.status(201).body(book)` and you've
said exactly what status and what body the client should see.

```java
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
public class BookController {

    @PostMapping("/api/books")
    public ResponseEntity<Book> createBook(@RequestBody Book book) {
        Book saved = save(book);
        return ResponseEntity.status(201).body(saved);   // 201 Created, body is the new book
    }

    @GetMapping("/api/books/{id}")
    public ResponseEntity<Book> getBook(@PathVariable Long id) {
        Book found = findById(id);
        if (found == null) {
            return ResponseEntity.notFound().build();    // 404, no body
        }
        return ResponseEntity.ok(found);                 // 200, body is the book
    }
}
```

*What just happened:* `createBook` now returns a `ResponseEntity` with status 201 and the new book as the
body - the client learns the resource was *created*, not merely fetched. `getBook` checks whether the book
exists: if not, `ResponseEntity.notFound().build()` produces a clean 404 with no body; otherwise
`ResponseEntity.ok(found)` returns 200 with the book. Contrast this with the earlier methods that returned a
plain `Book` and always got 200 - `ResponseEntity` is the switch you flip when the default status isn't the
truth you want to tell.

💡 Use a plain return type when 200 is genuinely correct (a simple list or lookup that always succeeds), and
reach for `ResponseEntity` when the status varies - creation, deletion, not-found, or anything where the
client should react differently based on the code. Both are valid; pick the one that says what you mean.

## How the request flows

Seeing the whole path a request takes demystifies what feels like magic.

```mermaid
flowchart TD
  A[HTTP request arrives] --> B[DispatcherServlet]
  B --> C[Match URL + method to a controller method]
  C --> D[Bind path vars, params, and @RequestBody]
  D --> E[Call your method]
  E --> F[Jackson serializes the return value to JSON]
  F --> G[HTTP response sent back]
```

📝 At the front of every Spring web app sits one object you never wrote: the **DispatcherServlet**. Every
request hits it first. It looks at the URL and HTTP method, finds the controller method whose mapping
matches, **binds** the inputs (pulls the `id` from the path, the `author` from the query string, the JSON from
the body - converting types as it goes), and then calls your method. Whatever you return goes back through
Jackson to become the JSON response. Your job is just the middle box: one focused method.

💡 Notice how *thin* these controller methods are. The best ones read the request, hand off to something
that does the real work, and shape the response - that's it. Here `save`, `findById`, and `findByAuthor`
are placeholders, but in a real app that work belongs in a separate **service layer**, which
[Phase 6](06-service-layer-and-validation.md) introduces: a `BookService` bean, injected the way Phase 2
showed, that holds the actual logic. The controller speaks HTTP; the service holds the rules.

⚠️ **Don't put business logic or database calls directly in a controller.** It's tempting to dump a query or a
pricing calculation right into the handler because it "works." It does - until that logic needs testing
(you'd need a fake HTTP request to exercise it), reusing (a scheduled job can't send itself a request), or
changing (and now every change touches the doorway). A controller that does two jobs becomes the place every
bug hides. Keep it to HTTP in, HTTP out, and delegate the rest.

## Recap

1. **`@RestController` makes a class an HTTP doorway.** It's `@Controller` + `@ResponseBody`, so whatever your
   methods return is serialized to JSON by Jackson and sent back as the response body.
2. **Mapping annotations route requests to methods.** `@GetMapping("/api/books")` handles `GET /api/books`,
   `@PostMapping("/api/books")` handles `POST /api/books`; Spring calls your method when a matching request arrives.
3. **Path variables identify, request params filter.** `@PathVariable` binds `{id}` from the URL path (one
   specific book); `@RequestParam` binds query-string values like `?author=...` (filter or modify), and can
   be optional with `required = false`.
4. **`@RequestBody` turns incoming JSON into an object.** On a POST, Jackson deserializes the request body
   into a `Book` before your method runs, so you work with a populated Java object.
5. **`ResponseEntity` controls status and headers.** Return a plain object for a default 200, or a
   `ResponseEntity` for 201 Created, 404 Not Found, and anything else where the status is part of the answer.
6. **The flow:** DispatcherServlet → match URL + method → bind inputs → call your (thin) method → Jackson
   serializes the result → response. ⚠️ Keep business logic and DB calls out of the controller - that's the
   service layer's job, coming in [Phase 6](06-service-layer-and-validation.md).

## Quick check

Make sure the core controller ideas stuck:

```quiz
[
  {
    "q": "What does @RestController add on top of plain @Controller?",
    "choices": [
      "@ResponseBody behavior - method return values are serialized straight to the response body (JSON), instead of being treated as view/page names",
      "It connects the class to the database automatically",
      "It makes every method run inside a transaction",
      "Nothing - @RestController and @Controller are identical"
    ],
    "answer": 0,
    "explain": "@RestController is @Controller + @ResponseBody. The @ResponseBody part means whatever a method returns IS the response body (serialized to JSON by Jackson), which is exactly what you want for an API rather than rendering an HTML view."
  },
  {
    "q": "You want to fetch one specific book by its id from the URL /api/books/2. Which annotation binds that 2 to your method parameter?",
    "choices": [
      "@PathVariable, because the id is part of the URL path and identifies a specific resource",
      "@RequestParam, because all URL values use the same annotation",
      "@RequestBody, because the id travels in the request body",
      "@GetMapping, because that annotation reads the value for you"
    ],
    "answer": 0,
    "explain": "A value embedded in the path (/api/books/{id}) is bound with @PathVariable - it identifies a resource. @RequestParam is for query-string values after the ? (like ?author=...), which filter or modify a request. @RequestBody is for the JSON body of a POST."
  },
  {
    "q": "Your create endpoint returns a plain Book object. A client reports it always gets HTTP 200 even though a resource was created. How do you return 201 Created instead?",
    "choices": [
      "Return a ResponseEntity, e.g. ResponseEntity.status(201).body(saved), which carries the status code alongside the body",
      "Add @PostMapping(status = 201) to the method",
      "Throw an exception after saving so Spring picks a different code",
      "Nothing can change it - controllers can only return 200"
    ],
    "answer": 0,
    "explain": "Returning a bare object gives the default 200. To control the status (and headers), wrap the body in a ResponseEntity - ResponseEntity.status(201).body(saved) reports 201 Created. The same tool gives you 404 via ResponseEntity.notFound().build()."
  }
]
```


---

# Configuration & Profiles

In [Phase 3](03-rest-controllers.md) you built a REST API that returns JSON. Every value in it was
*hardcoded* - the port, any URL, any tuning knob. Fine for a demo; it falls apart the moment the same code
has to run on your laptop, a staging box, and production, where each needs *different* values: a different
database, different log levels, different secrets.

**Configuration is how one build of your app adapts to many environments without recompiling.** You don't
ship three versions of the app - you ship *one* jar and feed it different settings depending on where it
lands. Spring Boot has a rich, layered system for exactly that, and once you see the layers you'll stop
being surprised by "why is it using *that* value?"

## The config file: `application.properties` / `application.yml`

📝 Spring Boot automatically looks for a config file named `application` on startup - no wiring, no annotation. Drop it in `src/main/resources/` and Boot reads it. You get two formats, and you pick one.

The older format is **`.properties`** - flat `key=value` lines:

```properties
server.port=8081
spring.application.name=bookstore
logging.level.org.springframework.web=DEBUG
```

*What just happened:* Three settings, each a dotted key and a value. `server.port` tells the embedded server to listen on 8081 instead of the default 8080. `spring.application.name` names your app (shows up in logs and tooling). The `logging.level...` line turns on DEBUG logging for Spring's web package so you can see request handling. These are Boot's own well-known keys - you didn't define them; Boot reads them.

The newer format is **`.yml`** (YAML), which expresses the same thing as a nested tree:

```yaml
server:
  port: 8081
spring:
  application:
    name: bookstore
logging:
  level:
    org.springframework.web: DEBUG
```

*What just happened:* Identical settings, written as indentation instead of repeated `server.`/`spring.` prefixes. The dotted key `server.port` becomes a `port:` nested under `server:`. YAML collapses the repetition, which is why it reads better as configs grow - once you have a dozen `spring.datasource.*` keys, the nested form is far easier to scan.

💡 **Use `.yml`.** Both formats are equivalent and Boot reads either, but YAML's nesting wins as soon as your config is more than a handful of lines. Just don't keep *both* files - pick one to avoid confusion about which wins. (One YAML gotcha: indentation is significant and must be spaces, never tabs.)

## Reading config in your code

A config file is useless if your code can't see the values. Spring gives you two ways in, and they suit different needs.

The quick one is **`@Value`** - inject a single value by its key:

```java
@Service
public class GreetingService {

    @Value("${app.greeting}")
    private String greeting;

    public String greet(String name) {
        return greeting + ", " + name + "!";
    }
}
```

With this in `application.yml`:

```yaml
app:
  greeting: "Hello"
```

*What just happened:* The `${app.greeting}` placeholder tells Spring "find the `app.greeting` key in the config and inject its value into this field." When the `GreetingService` bean is created (recall beans and injection from [Phase 2](02-dependency-injection-and-beans.md)), Spring resolves the placeholder and sets `greeting` to `"Hello"`. Change the file, restart, and the behavior changes - no code edit. `@Value` is perfect for one or two stray values.

📝 But when you have a *group* of related settings, the cleaner way is **`@ConfigurationProperties`** - it binds a whole block of config into one typed object. You define a class whose fields mirror the keys, and Spring populates it:

```java
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String name;
    private String greeting;
    private int maxResults;

    // getters and setters required for binding
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getGreeting() { return greeting; }
    public void setGreeting(String greeting) { this.greeting = greeting; }
    public int getMaxResults() { return maxResults; }
    public void setMaxResults(int maxResults) { this.maxResults = maxResults; }
}
```

Bound to this config:

```yaml
app:
  name: bookstore
  greeting: "Hello"
  max-results: 50
```

*What just happened:* `@ConfigurationProperties(prefix = "app")` says "take everything under the `app.` prefix and map it onto this object's fields by name." `app.name` lands in `name`, `app.greeting` in `greeting`, and `app.max-results` in `maxResults` - Spring's *relaxed binding* matches kebab-case to camelCase automatically. Crucially, `max-results: 50` becomes an `int`, not a string: the binding is **type-safe**, so a non-numeric value fails loudly at startup instead of blowing up later. Inject `AppProperties` like any other bean and call `appProps.getMaxResults()`.

💡 **Prefer `@ConfigurationProperties` for anything more than a value or two.** You get one typed object instead of scattered `@Value` strings, IDE autocomplete on the fields, type checking, and a single place that documents what your app can be configured with. Reach for `@Value` only for the odd one-off.

## Externalized configuration & precedence

Here's the idea that makes the single-jar dream work. 📝 Spring Boot doesn't read config from *one* place - it layers it from many sources and merges them, with later sources **overriding** earlier ones. Roughly, lowest to highest priority:

1. Defaults baked into the framework
2. Your `application.yml` (packaged in the jar)
3. OS **environment variables**
4. **Command-line arguments** (`--key=value`)

A setting from a higher layer wins over the same setting from a lower one. That single rule is what lets the *same* jar behave differently everywhere: ship `application.yml` with sensible defaults, then *override* the few values that differ per environment from the outside - no rebuild.

Say your jar bakes in `server.port: 8081`, but production needs 9000. You override it without touching the file:

```bash
java -jar bookstore.jar --server.port=9000
```

Or via an environment variable:

```bash
SERVER_PORT=9000 java -jar bookstore.jar
```

*What just happened:* In both cases the file says 8081, but the external source sits higher in the precedence order, so the app boots on 9000. The command-line `--server.port=9000` maps straight to the `server.port` key; the env var `SERVER_PORT` does too - Spring translates it back. This is the everyday way config reaches a containerized app: the image holds the jar with its defaults, and the deployment environment injects the overrides.

⚠️ **Watch the env-var naming.** Environment variables can't contain dots, so Spring uses *relaxed binding* in reverse: uppercase the key and replace dots with underscores. `app.name` becomes `APP_NAME`; `server.port` becomes `SERVER_PORT`; `spring.datasource.url` becomes `SPRING_DATASOURCE_URL`. Get the translation wrong and your override silently does nothing - the app keeps the file value and you waste an afternoon wondering why. For the broader why-and-how of environment-based config, see [/guides/env-vars-and-config](/guides/env-vars-and-config).

## Profiles: a config set per environment

Overriding values one at a time is fine for a couple of settings. But dev and prod often differ in *many* ways at once - different database, different log levels, different feature flags. 📝 **Profiles** let you bundle a whole set of config under a name and switch the entire set on or off together.

The mechanism is naming: alongside `application.yml`, you create `application-dev.yml` and `application-prod.yml`. Whatever profile is *active*, Boot loads its file *on top of* the base `application.yml` (so the base holds shared defaults, the profile file holds the differences).

`application-dev.yml`:

```yaml
logging:
  level:
    root: DEBUG
spring:
  datasource:
    url: jdbc:h2:mem:devdb
```

`application-prod.yml`:

```yaml
logging:
  level:
    root: WARN
spring:
  datasource:
    url: jdbc:postgresql://db.internal:5432/bookstore
```

*What just happened:* Two complete config sets. The dev profile points at a throwaway in-memory H2 database and logs verbosely; the prod profile points at a real PostgreSQL server and keeps logs quiet. They share whatever's in the base `application.yml`. You activate one and Boot layers the right file automatically.

You can also gate *beans* by profile with `@Profile`, so a whole component only exists in certain environments:

```java
@Component
@Profile("dev")
public class DevDataSeeder {
    // populates fake data on startup - only when "dev" is active
}
```

*What just happened:* `@Profile("dev")` tells Spring to create this bean **only** when the `dev` profile is active. In prod it does not exist at all, so your fake-data seeder can never accidentally run against the real database. This is the clean way to make behavior - not just values - environment-specific.

You choose the active profile the same way you override any other setting:

```bash
java -jar bookstore.jar --spring.profiles.active=prod
```

```console
... : The following 1 profile is active: "prod"
... : Tomcat started on port(s): 8081 (http)
... : Started BookstoreApplication in 2.1 seconds
```

*What just happened:* `--spring.profiles.active=prod` switched on the prod profile, so Boot loaded `application-prod.yml` over the base - the startup log confirms `"prod"` is active. In a real deployment you'd usually set this with `SPRING_PROFILES_ACTIVE` instead, so the platform decides the profile. No active profile? Boot runs with just the base `application.yml`.

## Secrets: the one thing that never goes in the file

There's a category of config that needs special care: passwords, API keys, database credentials, signing keys. ⚠️ **Never commit these to `application.yml`.** That file lives in your git repository, and anything in git is effectively public forever - even if you delete it later, it sits in the history. A leaked database password or cloud key in a repo is one of the most common, most expensive security mistakes there is.

The fix follows directly from precedence: leave secrets *out* of the file and inject them from outside at runtime - exactly the environment-variable mechanism you just saw.

```yaml
spring:
  datasource:
    url: jdbc:postgresql://db.internal:5432/bookstore
    username: bookstore_app
    password: ${DB_PASSWORD}
```

*What just happened:* The non-secret connection details live in the file, but the password is a `${DB_PASSWORD}` placeholder. At startup Spring resolves it from the `DB_PASSWORD` environment variable, which the deployment platform supplies. The real secret is never written down in the repo. For production-grade handling - rotation, vaults, managed secret stores - see [/guides/secrets-management](/guides/secrets-management).

💡 Config, profiles, and externalized secrets are three angles on one principle: *one build adapts to dev, staging, and prod by changing inputs, not code.* Compile and test a single artifact, then let the environment decide the port, database, log level, and secrets.

## Recap

1. Spring Boot auto-loads a config file named `application` from `src/main/resources/`. Prefer **`.yml`** over `.properties` - same capability, but nesting scales better. (YAML uses spaces, never tabs.)
2. Read config in code with **`@Value("${key}")`** for one-off values, or **`@ConfigurationProperties(prefix = ...)`** to bind a whole group into a typed object. Prefer `@ConfigurationProperties` - it's type-safe and self-documenting.
3. Config is **layered**: defaults < `application.yml` < environment variables < command-line args, with higher layers overriding lower. This is what lets one jar run in every environment.
4. **Environment variables** map to keys by uppercasing and replacing dots with underscores (`app.name` ↔ `APP_NAME`). Get the name wrong and the override silently does nothing.
5. **Profiles** (`application-dev.yml`, `application-prod.yml`, `@Profile("dev")`) bundle a full config set per environment; activate with `--spring.profiles.active=prod` or `SPRING_PROFILES_ACTIVE`.
6. **Never commit secrets** to the config file. Use a `${PLACEHOLDER}` and inject from environment variables or a secrets manager. One build, many environments - driven by inputs, not recompiles.

## Quick check

Make sure the config model stuck before you wire a database to it in the next phase:

```quiz
[
  {
    "q": "You have server.port: 8081 in application.yml but start the app with --server.port=9000. What port does it use, and why?",
    "choices": [
      "9000 - command-line arguments sit higher in the precedence order than the config file, so they override it",
      "8081 - the file is always authoritative once the app is built",
      "It fails to start because two sources disagree on the same key",
      "Whichever was set first wins, so 8081"
    ],
    "answer": 0,
    "explain": "Spring layers config from many sources with later ones overriding earlier ones: defaults < application.yml < env vars < command-line args. The command-line override wins, so the app boots on 9000 - which is exactly how one jar runs in many environments."
  },
  {
    "q": "Why is @ConfigurationProperties usually preferred over @Value for a group of related settings?",
    "choices": [
      "It binds a whole prefixed block into one typed object - type-safe, IDE-friendly, and a single documented place for your settings",
      "It is the only way to read config files at all",
      "It makes the application start faster",
      "It encrypts the values automatically"
    ],
    "answer": 0,
    "explain": "@Value injects single string values one at a time. @ConfigurationProperties maps a whole prefix onto a typed object, so you get type checking (a bad number fails at startup), autocomplete, and one place that documents what's configurable."
  },
  {
    "q": "Where should a production database password live?",
    "choices": [
      "Out of the config file entirely - injected from an environment variable or secrets manager via a ${PLACEHOLDER}",
      "Directly in application.yml so it ships with the jar",
      "In application-prod.yml, which is safe because it's profile-specific",
      "Hardcoded in the Java source so it can't be changed by accident"
    ],
    "answer": 0,
    "explain": "Any file in your repo - including application-prod.yml - is committed to git and effectively public forever. Secrets must stay out of the build: use a ${DB_PASSWORD} placeholder and supply the real value from the environment or a secrets manager at runtime."
  }
]
```


---

# Persistence with Spring Data JPA

Up to now your API has been making things up. A request comes in, you build a `Book` in memory, hand it
back, and the moment the process restarts, it's gone. Real apps remember - they write to a database that
survives restarts, deploys, and crashes. This phase teaches your Spring app to *persist*, and it's where a
famous piece of Spring magic shows up: you'll define a repository interface with **no implementation**,
and Spring writes the implementation for you at runtime.

That magic is wonderful right up until it surprises you. Before any annotations, let's build the mental
model of what's actually happening underneath - the day the magic bites, the only thing that saves you is
knowing what it was hiding.

## The mental model: layers all the way down

📝 The core idea is an **ORM** - Object-Relational Mapping. Your Java world is made of *objects* (`Book`
instances with fields). Your database world is made of *rows* in *tables* (a `book` table with columns). An
ORM is the translator that maps one onto the other: you work with objects in your code, and the ORM turns
your object operations into the SQL that reads and writes rows. You save a `Book` object; the ORM emits an
`INSERT`. You ask for a book by id; the ORM emits a `SELECT` and hands you back a `Book`.

If the words *table*, *row*, and *column* feel fuzzy, take ten minutes with
[/guides/what-a-database-is](/guides/what-a-database-is) first - this phase assumes you know what a row is and
that databases speak SQL.

Here's the stack you're standing on, top to bottom:

```mermaid
flowchart TD
  A[Your code: save a Book object] --> B[Spring Data JPA<br/>generates repositories]
  B --> C[JPA<br/>the standard / API]
  C --> D[Hibernate<br/>the ORM implementation]
  D --> E[JDBC<br/>raw SQL over a connection]
  E --> F[(Database)]
```

Four names, one job. **JPA** (Jakarta Persistence API) is the *standard* - a set of interfaces and
annotations that describe how Java objects map to tables. **Hibernate** is the most common *implementation*
of that standard: the actual ORM engine that generates SQL. **JDBC** is the low-level Java API Hibernate uses
to send that SQL over a database connection. And **Spring Data JPA** sits on top of all of it, removing the
last layer of boilerplate - the repository code you'd otherwise hand-write to call JPA.

💡 You only ever *write* against the top layer. But every layer below is still running, and when something is
slow or wrong, you debug *downward* - from your object call, to the SQL Hibernate emitted, to what the
database actually did. Keep this ladder in your head; we'll climb back down it at the end of the phase.

## `@Entity` - mapping a Book to a table

The first job is to tell JPA "this Java class corresponds to a database table." You do that with annotations.

```java
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String author;
    private String isbn;

    protected Book() {}   // JPA requires a no-arg constructor

    public Book(String title, String author, String isbn) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
    }

    public Long getId() { return id; }
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public String getIsbn() { return isbn; }

    public void setTitle(String title) { this.title = title; }
    public void setAuthor(String author) { this.author = author; }
    public void setIsbn(String isbn) { this.isbn = isbn; }
}
```

*What just happened:* `@Entity` marks `Book` as a persistent type - JPA now knows it maps to a table (named
`book` by default). `@Id` names the primary key field, and `@GeneratedValue(strategy = GenerationType.IDENTITY)`
tells JPA the database generates the id for you, so you never set it by hand. The plain fields each become
a column with the same name. The no-arg constructor exists because Hibernate constructs entities
*reflectively* when it reads a row back - it makes a blank `Book` and fills the fields.

The table this maps to looks like:

```sql
CREATE TABLE book (
    id     BIGINT       NOT NULL AUTO_INCREMENT PRIMARY KEY,
    title  VARCHAR(255),
    author VARCHAR(255),
    isbn   VARCHAR(255)
);
```

*What just happened:* Each field became a column, `id` became the auto-incrementing primary key matching
`GenerationType.IDENTITY`, and the `String` fields became `VARCHAR`. In development you can have Hibernate
generate this table for you; in production you'll manage the schema yourself with migrations - but the
*shape* is exactly this.

⚠️ **Why a mutable class and not a `record`?** Records are the perfect immutable data carrier
([Records & Modern Java](/guides/java-from-zero/13-records-and-modern-java)), but JPA needs a no-arg
constructor (to build a blank instance before populating it) and mutable fields (to update them later, and
so Hibernate can swap in a *lazy proxy* for relationships) - the opposite of a record's immutable, all-args
design. Entities stay as plain classes with a no-arg constructor and setters. Records are still right for
the *DTOs* you'll add in [Phase 6](06-service-layer-and-validation.md); just wrong for entities.

## Repositories - where the magic happens

Now the part that feels like a trick. To get full create/read/update/delete access to the `book` table, you
write this:

```java
import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long> {
}
```

*What just happened:* That's the whole file - an **interface** with no fields, methods, or implementation.
`JpaRepository<Book, Long>` says "this is a repository for `Book` entities whose id type is `Long`." At
startup, Spring Data JPA finds this interface and **generates a concrete implementation at runtime**, then
registers it as a bean. You never write the class; Spring writes and injects it for you. Inheriting from
`JpaRepository` gives you a pile of methods for free, including:

- `save(book)` - insert or update a row, returns the saved entity (now with its generated id)
- `findById(id)` - look up one row by primary key, returns an `Optional<Book>`
- `findAll()` - read every row
- `deleteById(id)` / `delete(book)` - remove a row
- `count()`, `existsById(id)` - quick aggregates

You wire it in exactly like any other bean (the dependency injection from
[Phase 2](02-dependency-injection-and-beans.md)):

```java
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class BookSeeder {

    private final BookRepository books;

    public BookSeeder(BookRepository books) {   // Spring injects the generated impl
        this.books = books;
    }

    public void run() {
        Book saved = books.save(new Book("Dune", "Frank Herbert", "9780441013593"));
        System.out.println("saved with id = " + saved.getId());

        List<Book> all = books.findAll();
        System.out.println("books in table = " + all.size());
    }
}
```

```console
saved with id = 1
books in table = 1
```

*What just happened:* `books.save(...)` took a brand-new `Book` (no id yet), inserted a row, and returned the
same book *with its database-generated id filled in* - that's why `saved.getId()` is `1`. Then `findAll()`
read the table back. You called two methods on an interface you never implemented, and real SQL ran against a
real database. That's Spring Data JPA earning its keep.

## Derived query methods - Spring reads your method names

`findById` and `findAll` cover the basics, but real apps ask sharper questions: *all books by this author*,
*books whose title contains a search term*, *does a book with this ISBN already exist?* You could write SQL
for each. You don't have to. Spring Data parses the **name of the method** into a query.

Add method declarations to the interface - still no bodies:

```java
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface BookRepository extends JpaRepository<Book, Long> {

    List<Book> findByAuthor(String author);

    List<Book> findByTitleContainingIgnoreCase(String query);

    boolean existsByIsbn(String isbn);
}
```

*What just happened:* You declared three methods and wrote zero implementations. Spring Data reads each
name as a tiny query language. `findByAuthor` → "select books where `author` equals the argument."
`findByTitleContainingIgnoreCase` → "where `title` contains the argument, case-insensitively" (a `LIKE`
with wildcards, lower-cased both sides). `existsByIsbn` → "is there any row whose `isbn` equals the
argument?", returning a `boolean`. The keywords (`findBy`, `existsBy`, `Containing`, `IgnoreCase`, `And`,
`Or`, `OrderBy`...) compose, and Spring builds the query from them.

Here's `findByAuthor` in use and the SQL Hibernate emits for it:

```java
List<Book> herbertBooks = books.findByAuthor("Frank Herbert");
```

```sql
SELECT b.id, b.title, b.author, b.isbn
FROM book b
WHERE b.author = ?
```

*What just happened:* The method name became a parameterized `SELECT ... WHERE author = ?`, and Spring bound
`"Frank Herbert"` to the `?` placeholder. (Parameterized - not string-concatenated - so it's safe from SQL
injection by construction.) You're showcasing `existsByIsbn` here for a reason:
[Phase 6](06-service-layer-and-validation.md) calls it to reject duplicate ISBNs before saving, so it lives
on the repository from the start.

💡 The magic is *method-name parsing*, and that's also its limit. The moment a query needs something the
naming convention can't express cleanly - a join, an aggregate, a condition too gnarly to spell as a method
name - stop fighting the name and write the query explicitly with `@Query`:

```java
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

@Query("SELECT b FROM Book b WHERE b.author = :author AND b.isbn IS NOT NULL")
List<Book> findCataloguedByAuthor(@Param("author") String author);
```

*What just happened:* `@Query` lets you write JPQL (a query language over your *entities*, not raw tables - 
note `Book`, the class, not `book`, the table) when a derived name would get unwieldy. You reach for this when
the method name stops being shorter than the query it stands for. (`nativeQuery = true` lets you drop to raw
SQL if you truly need a database-specific feature.)

## Where the magic stops - and bites

The plain part nobody puts on the brochure: **an ORM removes boilerplate, not the need to understand
your database.** The abstraction is leaky on purpose, because the database underneath is real. The first
time you treat JPA as a black box that "handles persistence," it will quietly do something expensive or
wrong. A few places it bites everyone:

**The N+1 query problem.** You load 100 books, then loop over them touching a related collection (say each
book's reviews). If that relationship is lazy, every `book.getReviews()` fires its *own* `SELECT` - so one
query to load the books becomes 1 + 100 = 101 queries. Your code looks like a simple loop; the database sees
a storm. The fix (a fetch join or an entity graph) requires knowing the storm is happening at all.

**Lazy vs. eager loading.** Relationships are loaded *lazily* by default - Hibernate hands you a proxy and
only hits the database when you actually touch the related data. That's good for performance but explodes if
you touch lazy data *after* the transaction has closed, with a `LazyInitializationException`. Which leads
straight to:

**`@Transactional` boundaries.** The window in which an entity is "live" and can lazy-load is the transaction.
Step outside it and the connection is gone. You'll meet `@Transactional` properly in
[Phase 6](06-service-layer-and-validation.md), but file this away now: *where* your transaction begins and
ends decides what your entities can and can't do.

**You still have to read the SQL.** The single most useful habit when JPA misbehaves is to make it show you
its work:

```yaml
spring:
  jpa:
    show-sql: true                 # log every SQL statement Hibernate emits
    properties:
      hibernate:
        format_sql: true           # pretty-print it
```

*What just happened:* Turning on `show-sql` logs the exact SQL Hibernate generates for every operation - 
how you *see* the N+1 storm instead of guessing, and confirm a derived method built the query you expected.
This is climbing back down the ladder: object call → generated SQL → database. When a query is slow, that
SQL is your starting point - see [/guides/why-is-my-query-slow](/guides/why-is-my-query-slow).

💡 Spring Data JPA is a fantastic productivity tool *and* a thin layer over a database you're still
responsible for. Let it write the boilerplate; don't let it talk you out of knowing what SQL it wrote.

## Recap

1. **An ORM maps objects to rows.** You work with `Book` objects; JPA/Hibernate translates that into SQL.
   The stack is Spring Data JPA → JPA (the standard) → Hibernate (the implementation) → JDBC → the database.
2. **`@Entity` maps a class to a table.** `@Id` + `@GeneratedValue` define the generated primary key; plain
   fields become columns. ⚠️ Entities are mutable classes with a no-arg constructor - *not* records, because
   the ORM needs to build blank instances and mutate fields.
3. **A repository is a magic interface.** `interface BookRepository extends JpaRepository<Book, Long>` gets a
   runtime-generated implementation with `save`, `findById`, `findAll`, `delete`, and more - you write no code.
4. **Derived queries come from method names.** `findByAuthor`, `findByTitleContainingIgnoreCase`, and
   `existsByIsbn` are parsed into SQL from their names; reach for `@Query` (JPQL) when a name gets unwieldy.
5. **The abstraction is leaky on purpose.** ⚠️ N+1 queries, lazy-vs-eager loading, and `@Transactional`
   boundaries all bite if you treat JPA as a black box - turn on `spring.jpa.show-sql=true` and read the SQL
   it emits. An ORM saves boilerplate, not the need to understand your database.

## Quick check

Make sure the model - and where it leaks - actually stuck:

```quiz
[
  {
    "q": "How does Spring Data JPA provide the implementation of BookRepository when the interface has no method bodies?",
    "choices": [
      "It generates a concrete implementation at runtime and registers it as a bean you can inject",
      "You must write the implementation class yourself before the app will start",
      "It copies a default implementation from JpaRepository into your source files",
      "It stores the queries in the database and runs them as stored procedures"
    ],
    "answer": 0,
    "explain": "Spring Data JPA finds repository interfaces at startup and generates concrete implementations at runtime, registering each as a bean. That's why you extend JpaRepository and write no code - Spring writes and injects the implementation for you."
  },
  {
    "q": "Why is a JPA @Entity written as a mutable class with a no-arg constructor instead of a Java record?",
    "choices": [
      "Hibernate needs to build a blank instance and then set its fields (and swap in lazy proxies), which a record's immutable, all-args design doesn't allow",
      "Records cannot be annotated with @Entity at all in any version of Java",
      "Records are slower to serialize to JSON than mutable classes",
      "JPA requires every entity field to be public, and records make fields private"
    ],
    "answer": 0,
    "explain": "The ORM constructs entities reflectively (a no-arg constructor) and mutates their fields when reading rows and managing lazy relationships. A record is immutable with an all-args constructor - the opposite of what JPA needs. Records are still ideal for DTOs, just not entities."
  },
  {
    "q": "What does the derived query method `existsByIsbn(String isbn)` cause Spring Data to generate?",
    "choices": [
      "A query that checks whether any book row has a matching isbn, returning a boolean",
      "A method that inserts a new book if the isbn does not already exist",
      "A list of every book sorted by isbn",
      "Nothing - exists-style methods require a hand-written @Query"
    ],
    "answer": 0,
    "explain": "Spring Data parses the method name: existsBy + Isbn becomes a query asking whether any row's isbn equals the argument, returning a boolean. Phase 6 uses exactly this to reject duplicate ISBNs before saving."
  }
]
```


---

# The Service Layer, DTOs & Validation

By now your API works. Back in [Phase 3](03-rest-controllers.md) you wrote a `BookController` that took
HTTP requests, and in [Phase 5](05-persistence-with-jpa.md) you wired a `BookRepository` so those requests
hit a database. If you followed along literally, your controller probably calls the repository directly - 
request comes in, controller saves a row, controller returns it. For three endpoints, that's fine. It's
also a trap, worth seeing before it closes.

The mental model: **separation of concerns**. Each piece of your app should have exactly one job, and hand
off to the next piece for everything else. A controller's one job is to speak HTTP - not validate a book,
calculate a discount, enforce that ISBNs are unique, or manage a database transaction. The moment a
controller does two of those things, it becomes the place every future change has to touch, and every bug
hides. This phase introduces the three layers that fix that, plus the validation that guards the front door.

## Why a service layer

📝 A well-structured Spring app has **three layers**, and a request flows through them in order:

- **Controller** - speaks HTTP. Reads the request, calls down, shapes the response. Knows about status
  codes and JSON. Knows *nothing* about business rules.
- **Service** - holds the business logic. "A book can't be published in the future." "Deleting a book
  archives it instead." This is where the actual decisions live, in plain `@Service` beans.
- **Repository** - talks to the database. You met this in [Phase 5](05-persistence-with-jpa.md); it knows
  rows and queries and nothing else.

Here's the shape of it:

```mermaid
flowchart TD
  Client[HTTP client] -->|JSON request| Controller
  Controller -->|calls| Service
  Service -->|calls| Repository
  Repository -->|SQL| DB[(Database)]
  DB -->|rows| Repository
  Repository -->|entities| Service
  Service -->|DTOs| Controller
  Controller -->|JSON response| Client
```

The rule that makes this work: **each layer only talks to the one directly below it.** Controllers never
touch repositories. Services never touch HTTP. The dependency arrows point one direction.

💡 Why bother, when calling the repository from the controller "works"? Because the controller is the
worst possible home for logic. It's hard to test (you need a fake HTTP request to exercise a price
calculation), hard to reuse (a scheduled job can't send itself an HTTP request to run the same rule), and
quietly accumulates responsibilities until nobody can read it. A `@Service` is a plain bean you can call
from a controller, a job, a message listener, or a test - directly, no HTTP required.

Moving the logic out of the Phase 3 controller - here's the "everything in the controller" version we're
leaving behind:

```java
@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookRepository books;

    public BookController(BookRepository books) {
        this.books = books;
    }

    @PostMapping
    public Book create(@RequestBody Book book) {
        // business rule jammed into the controller:
        if (books.existsByIsbn(book.getIsbn())) {
            throw new IllegalStateException("duplicate ISBN");
        }
        return books.save(book);
    }
}
```

*What just happened:* The controller is doing three jobs at once - receiving HTTP, enforcing a uniqueness
rule, and persisting. That uniqueness check is business logic standing in the doorway. Now we extract it
into a service the controller can call:

```java
@Service
public class BookService {

    private final BookRepository books;

    public BookService(BookRepository books) {
        this.books = books;
    }

    public Book create(Book book) {
        if (books.existsByIsbn(book.getIsbn())) {
            throw new DuplicateIsbnException(book.getIsbn());
        }
        return books.save(book);
    }
}
```

```java
@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookService service;

    public BookController(BookService service) {
        this.service = service;
    }

    @PostMapping
    public Book create(@RequestBody Book book) {
        return service.create(book);   // controller just delegates
    }
}
```

*What just happened:* `@Service` registers `BookService` as a bean (same dependency injection from
[Phase 2](02-dependency-injection-and-beans.md)), so Spring constructs it and injects the repository, then
injects the *service* into the controller. The controller's `create` method shrank to a single line:
receive, delegate, return. The duplicate-ISBN rule now lives in one place a test can call directly with a
plain `Book` object - no HTTP needed. (`DuplicateIsbnException` becomes a clean 409 in
[Phase 7](07-error-handling.md); for now it just signals the problem.)

## `@Transactional` - all or nothing

📝 A **transaction** is a unit of work that either fully commits or fully rolls back - there is no
"halfway." Imagine an operation that saves a book *and* decrements an inventory count in two separate
writes. If the second write fails, you do not want the first one to stick around: you'd have a book with no
matching inventory record, and your data would be quietly lying to you. A transaction guarantees that both
happen or neither does.

In Spring you don't manage transactions by hand. You annotate a method, and Spring opens a transaction
before it runs and commits when it returns (or rolls back if it throws). The natural home for
`@Transactional` is the **service layer**, because the service is where a "unit of work" is defined - one
business operation, possibly several writes.

```java
@Service
public class BookService {

    private final BookRepository books;
    private final InventoryRepository inventory;

    public BookService(BookRepository books, InventoryRepository inventory) {
        this.books = books;
        this.inventory = inventory;
    }

    @Transactional   // both writes commit together, or neither does
    public Book createWithStock(Book book, int initialStock) {
        Book saved = books.save(book);
        inventory.save(new Inventory(saved.getId(), initialStock));
        return saved;
    }

    @Transactional(readOnly = true)   // a hint: this method only reads
    public List<Book> findAll() {
        return books.findAll();
    }
}
```

*What just happened:* On `createWithStock`, Spring opens a transaction, runs both saves, and commits only
if the method finishes cleanly. If `inventory.save(...)` throws, the book save is rolled back too - no
half-written state. On `findAll`, `readOnly = true` tells Spring (and the JDBC driver) this method won't
modify anything, which lets the database skip bookkeeping and perform better. Use `readOnly = true` for
query methods, plain `@Transactional` for methods that write.

⚠️ Two gotchas that bite everyone at least once:

- **Self-invocation skips it.** `@Transactional` works through a Spring **proxy** - Spring wraps your bean
  in a generated object that starts the transaction *before handing the call to your real method*. That
  only happens when the call comes from *outside* the bean. If method `a()` in `BookService` calls
  `this.b()` (where `b()` is `@Transactional`), the call never leaves the object, never passes through the
  proxy, and `b()` runs with **no transaction at all** - silently. The fix: call across a bean boundary
  (put `b()` in another service), not `this.b()`.
- **By default, only unchecked exceptions roll back.** Spring rolls back automatically on
  `RuntimeException` and `Error`, but *not* on checked exceptions, which it lets commit. If a method that
  throws a checked exception should roll back, say so explicitly:
  `@Transactional(rollbackFor = SomeCheckedException.class)`.

## DTOs vs entities - don't expose your database

So far the controller has been accepting and returning `Book` - the JPA `@Entity` from
[Phase 5](05-persistence-with-jpa.md). That's the second trap, and it's a common one.

📝 A **DTO** (Data Transfer Object) is the shape you expose over your API - the JSON a client sends and
receives. An **entity** is the shape you *store* - a class mapped to a database table. These are two
different concerns that happen to look similar today, and the whole point of a DTO is to keep them
*separate* so they can evolve independently. A Java `record` (see
[Records & Modern Java](/guides/java-from-zero/13-records-and-modern-java)) is the perfect tool for a DTO:
it's an immutable data carrier in one line, exactly what a request or response body wants to be.

Why pay for the extra classes? Three concrete reasons:

- **Don't leak database internals.** Your entity might have an internal `createdBy` audit field, a
  soft-delete flag, or a password hash. Serialize the entity straight to JSON and you've published all of
  it. A DTO exposes only the fields you *chose* to expose.
- **Avoid over-posting.** If clients send a raw `Book` entity, a malicious or careless request can set
  fields you never meant them to - an `id`, an `internalRating`, a `featured` flag. With a DTO, only the
  fields *on the DTO* can ever be set; everything else is impossible by construction.
- **Decouple the API from the schema.** Rename a column or split a table tomorrow, and your public API
  shouldn't even flinch. The DTO is a contract with your clients; the entity is an implementation detail.
  Keep them apart and you can change one without breaking the other.

Here are the two shapes side by side, plus the mapping between them:

```java
// Entity - how we STORE a book (from Phase 5)
@Entity
public class Book {
    @Id @GeneratedValue
    private Long id;
    private String title;
    private String author;
    private String isbn;
    private String internalNotes;   // private - must never reach the API
    // getters / setters omitted
}

// DTOs - how we EXPOSE a book. Records: immutable, one line each.
public record CreateBookRequest(String title, String author, String isbn) {}

public record BookResponse(Long id, String title, String author, String isbn) {}
```

```java
@Service
public class BookService {

    private final BookRepository books;

    public BookService(BookRepository books) {
        this.books = books;
    }

    @Transactional
    public BookResponse create(CreateBookRequest req) {
        if (books.existsByIsbn(req.isbn())) {
            throw new DuplicateIsbnException(req.isbn());
        }
        Book entity = new Book();          // DTO -> entity (map only allowed fields)
        entity.setTitle(req.title());
        entity.setAuthor(req.author());
        entity.setIsbn(req.isbn());

        Book saved = books.save(entity);
        return toResponse(saved);          // entity -> DTO (expose only chosen fields)
    }

    private BookResponse toResponse(Book b) {
        return new BookResponse(b.getId(), b.getTitle(), b.getAuthor(), b.getIsbn());
    }
}
```

*What just happened:* The incoming `CreateBookRequest` has only the three fields a client is allowed to
set - no `id`, no `internalNotes` - so over-posting is impossible. The service maps that request onto a
fresh `Book` entity field by field, saves it, then maps the saved entity back into a `BookResponse` that
exposes only what we want public. `internalNotes` never appears in either DTO, so it can never leak.

⚠️ **Returning entities directly causes real pain, not just leakage.** A JPA entity often has *lazy*
relationships - a `Book` with a lazily-loaded `List<Review>` not fetched until you touch it. Hand that
entity to the JSON serializer and it tries to read every field, triggering surprise database queries
mid-serialization (or a `LazyInitializationException` once the transaction has closed). DTOs sidestep this
entirely: map exactly the data you want *inside* the transaction, and the serializer only sees a plain,
fully-populated record.

## Bean Validation - reject bad input at the door

A client just sent you a book with a blank title and an ISBN of `"x"`. Where do you catch that? Not with a
pile of `if` statements at the top of every method - that's the controller-doing-two-jobs problem again.
Spring has a declarative answer: **Bean Validation**, where you annotate the DTO with the rules its data
must satisfy, then ask Spring to enforce them automatically.

📝 You put constraint annotations on the DTO's fields, and add `@Valid` to the controller parameter. When a
request arrives, Spring validates the DTO *before your method body runs* - and if anything fails, your code
never executes; the client gets a 400 instead. The rules live with the data they describe, and the check
happens in one consistent place.

```java
import jakarta.validation.constraints.*;

public record CreateBookRequest(
    @NotBlank(message = "title is required")
    @Size(max = 200, message = "title must be at most 200 characters")
    String title,

    @NotBlank(message = "author is required")
    String author,

    @Pattern(regexp = "\\d{13}", message = "isbn must be 13 digits")
    String isbn,

    @Email(message = "must be a valid email")
    String contactEmail,

    @Min(value = 0, message = "price cannot be negative")
    int price
) {}
```

```java
@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookService service;

    public BookController(BookService service) {
        this.service = service;
    }

    @PostMapping
    public BookResponse create(@Valid @RequestBody CreateBookRequest req) {
        return service.create(req);   // only runs if validation passed
    }
}
```

*What just happened:* The constraints (`@NotBlank`, `@Size`, `@Pattern`, `@Email`, `@Min`) describe the
valid shape of a request right on the record. `@Valid` on the `@RequestBody` parameter is the switch that
tells Spring to actually run those checks before calling `create`. Send a good request and it flows
straight through; send a bad one and your method body is never entered.

When validation fails, Spring throws a `MethodArgumentNotValidException`, which by default produces a 400
Bad Request. The raw response looks roughly like this:

```json
{
  "timestamp": "2026-06-22T10:15:30.123+00:00",
  "status": 400,
  "error": "Bad Request",
  "path": "/api/books",
  "errors": [
    { "field": "title", "defaultMessage": "title is required" },
    { "field": "isbn",  "defaultMessage": "isbn must be 13 digits" }
  ]
}
```

*What just happened:* Without writing a single `if`, the bad request was rejected with a 400 and a list of
exactly which fields failed and why - your `message` strings come straight back to the client. The default
shape is a bit noisy and not something you'd ship as-is; in [Phase 7](07-error-handling.md) you'll catch
`MethodArgumentNotValidException` in one place and format a clean, consistent error body.

## Tie it together

Look at the full path a `POST /api/books` now travels, each step owned by exactly one layer:

```mermaid
flowchart LR
  A[Controller: @Valid checks DTO] --> B[Controller maps to / passes DTO]
  B --> C[Service: @Transactional logic]
  C --> D[Service maps DTO to Entity]
  D --> E[Repository: save]
  E --> F[Service maps Entity to DTO]
  F --> G[Controller returns JSON]
```

The controller validates and translates HTTP. The service enforces the rules and owns the transaction. The
repository moves rows. Nothing knows more than it needs to.

💡 This layering is the difference between a Spring app you can grow and one you'll rewrite. Each layer has
a single, testable job: unit-test the service with a fake repository and zero HTTP, slice-test the
controller's validation without a database, and trust the repository because it's the framework's code, not
yours. That testability is what [Phase 8](08-testing-spring-boot.md) builds on - and the clean exceptions
your service throws are what [Phase 7](07-error-handling.md) turns into correct HTTP responses next.

## Recap

1. **Three layers, one job each.** Controllers speak HTTP, services hold business logic, repositories talk
   to the database - and each layer only calls the one below it. Logic in the controller is the trap this
   structure fixes.
2. **`@Service` beans hold your logic.** Move rules out of the controller into a plain bean you can call
   from anywhere - a controller, a job, or a test - with no HTTP required.
3. **`@Transactional` makes a method all-or-nothing.** Put it on service methods that write; use
   `readOnly = true` for queries. ⚠️ It works only through Spring's proxy (so `this.method()` self-calls
   skip it), and by default only unchecked exceptions trigger a rollback.
4. **DTOs decouple your API from your database.** A `record` DTO exposes only chosen fields - no leaking
   internals, no over-posting, no coupling the public contract to the schema. ⚠️ Returning entities
   directly invites lazy-loading and serialization bugs.
5. **Bean Validation guards the door.** Annotate the DTO (`@NotBlank`, `@Size`, `@Email`, `@Min`,
   `@Pattern`) and add `@Valid` on the controller parameter; Spring rejects bad input with a 400 before
   your code runs.
6. **The flow:** controller validates → maps to/passes a DTO → service (transactional) runs the logic →
   repository persists → mapped back to a DTO → returned as JSON. One job per layer is what makes the app
   testable and maintainable.

## Quick check

Make sure the layering and its two famous gotchas stuck:

```quiz
[
  {
    "q": "Why does a @Transactional method silently run with no transaction when called via this.method() from inside the same bean?",
    "choices": [
      "Because @Transactional works through a Spring proxy, and a self-invocation never leaves the object to pass through that proxy",
      "Because @Transactional only applies to controller methods, not service methods",
      "Because transactions are disabled by default and must be turned on in application.yml",
      "Because the method needs to be public and static for the annotation to take effect"
    ],
    "answer": 0,
    "explain": "Spring wraps the bean in a proxy that opens the transaction before delegating to your real method. That interception only happens for calls coming from outside the bean. A this.method() call stays inside the object, bypasses the proxy, and runs untransacted. Call across a bean boundary instead."
  },
  {
    "q": "What is the main reason to return a DTO (e.g. a record) from your controller instead of the JPA @Entity?",
    "choices": [
      "It decouples the API from the database schema and prevents leaking internal fields, over-posting, and lazy-loading serialization bugs",
      "DTOs are saved to the database faster than entities",
      "Entities cannot be converted to JSON at all without a DTO",
      "It is required by Spring - controllers reject entity return types"
    ],
    "answer": 0,
    "explain": "The DTO is your public API contract; the entity is a storage detail. Keeping them separate lets you expose only chosen fields (no leaking internals or over-posting) and avoids lazy-loading/serialization problems - and lets the schema change without breaking clients."
  },
  {
    "q": "What makes Spring actually validate an incoming request body against the constraints on its DTO?",
    "choices": [
      "Adding @Valid to the @RequestBody controller parameter, alongside the constraint annotations on the DTO fields",
      "Annotating the service method with @Transactional",
      "Putting the constraint annotations on the entity instead of the DTO",
      "Nothing - Spring validates every request body automatically"
    ],
    "answer": 0,
    "explain": "The constraints (@NotBlank, @Size, etc.) describe the rules, but @Valid on the parameter is the switch that tells Spring to enforce them before your method body runs. Without @Valid, the annotations are just metadata and no validation happens."
  }
]
```


---

# Error Handling Done Right

Your API works on the happy path. In [Phase 6](06-service-layer-and-validation.md) the service started
throwing real exceptions when things go wrong - a `DuplicateIsbnException` when an ISBN already exists, and
Spring's own `MethodArgumentNotValidException` when `@Valid` rejects a bad request body. The question this
phase answers: **what does the client actually see when one of those is thrown?**

**An exception is not the answer the client gets - it's a signal you translate into one.** Your service
speaks in Java exceptions ("this book wasn't found," "this ISBN is a duplicate"). Your client speaks HTTP
("404," "409," "400"). Error handling is the layer that translates between those two languages. Skip it,
and Spring picks a translation for you - and it's bad.

If you've read the Java side of this, the exception machinery itself - throwing, catching, the
checked/unchecked split - is covered in [Errors & I/O](/guides/java-from-zero/07-errors-and-io). This phase
is about what Spring does with an exception once it escapes your code.

## The default is bad

Let's see what happens when you throw something and *don't* handle it. Suppose a `GET /api/books/999`
hits a book that doesn't exist, and your service throws:

```java
@Service
public class BookService {

    private final BookRepository books;

    public BookService(BookRepository books) {
        this.books = books;
    }

    public BookResponse findById(Long id) {
        Book book = books.findById(id)
            .orElseThrow(() -> new BookNotFoundException(id));   // nothing catches this
        return toResponse(book);
    }
}
```

*What just happened:* `findById` on the repository returns an `Optional<Book>` (the standard "maybe there's
a row, maybe not" wrapper). When it's empty, `orElseThrow` fires our `BookNotFoundException`. We never
catch it, so it unwinds straight out of the controller and into Spring's hands. Spring has to turn it into
*some* HTTP response - and with no instructions from you, here's the JSON it produces:

```json
{
  "timestamp": "2026-06-22T10:15:30.123+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/api/books/999"
}
```

*What just happened:* ⚠️ Spring's default for *any* uncaught exception is **500 Internal Server Error** - 
the status that means "the server broke." But the server didn't break; the client asked for a book that
isn't there. A 500 says "this is our fault, try again later," when the accurate answer is "that book doesn't
exist, stop asking." The status is a lie, and nothing in the body names *which* book or *why*. (Hit the
same endpoint from a browser and you may get the infamous **Whitelabel Error Page** instead of JSON.)

It gets worse in development: depending on your settings, that body can include `"trace"` with the full
Java stack trace, leaking your class names, file paths, and internal structure to anyone who pokes the API.
The default is bad on two counts - **wrong status** and **leaked or useless body**. Everything below fixes
both.

## Per-controller `@ExceptionHandler`

The narrowest fix: a method *inside a controller* that catches a specific exception type and turns it into
a clean response.

📝 An `@ExceptionHandler` method says "if this controller throws *this* exception type, don't let it
escape - run me instead, and my return value becomes the response." You annotate the method with the
exception class it handles and a `@ResponseStatus` for the HTTP code you want.

```java
@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookService service;

    public BookController(BookService service) {
        this.service = service;
    }

    @GetMapping("/{id}")
    public BookResponse get(@PathVariable Long id) {
        return service.findById(id);   // may throw BookNotFoundException
    }

    @ExceptionHandler(BookNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)            // 404
    public Map<String, Object> handleNotFound(BookNotFoundException ex) {
        return Map.of(
            "error", "book_not_found",
            "message", ex.getMessage()
        );
    }
}
```

*What just happened:* When `service.findById(id)` throws `BookNotFoundException`, Spring sees that this
controller has an `@ExceptionHandler` registered for exactly that type, so it calls `handleNotFound`
instead of falling back to the generic 500. The `@ResponseStatus(HttpStatus.NOT_FOUND)` sets the status to
**404**, and the returned map becomes the JSON body. Now the client gets:

```json
{
  "error": "book_not_found",
  "message": "No book found with id 999"
}
```

*What just happened:* A correct status (404, "that's not here"), and a body that names the problem. The catch:
this handler lives in `BookController` and only covers `BookController`. Add an `AuthorController` tomorrow
that also throws `BookNotFoundException`, and you'd have to copy the handler there too. Per-controller
handlers are fine for a one-off, controller-specific case - but for anything your whole app shares, copying
them everywhere is exactly the duplication the next section kills.

## Global handling with `@RestControllerAdvice`

📝 A class annotated `@RestControllerAdvice` is a **single, app-wide home for exception handling**. Every
`@ExceptionHandler` method inside it applies to *every* controller in the application. You write the
exception→response mapping once, and the whole API gets consistent errors for free.

(`@ControllerAdvice` is the same idea for server-rendered views; `@RestControllerAdvice` is just
`@ControllerAdvice` + `@ResponseBody`, so the return values are serialized to JSON - that's the one you
want for a REST API.)

Here's one class that handles the three cases every API needs: not-found, validation failure, and a
catch-all fallback.

```java
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 1. Our own "not found" -> 404
    @ExceptionHandler(BookNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Map<String, Object> handleNotFound(BookNotFoundException ex) {
        return Map.of("error", "book_not_found", "message", ex.getMessage());
    }

    // 2. Bean Validation failure (the @Valid from Phase 6) -> 400, with per-field detail
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleValidation(MethodArgumentNotValidException ex) {
        Map<String, String> fields = new HashMap<>();
        for (FieldError fe : ex.getBindingResult().getFieldErrors()) {
            fields.put(fe.getField(), fe.getDefaultMessage());
        }
        return Map.of("error", "validation_failed", "fields", fields);
    }

    // 3. Anything we didn't anticipate -> 500 (but a CLEAN 500, no stack trace leaked)
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, Object> handleUnexpected(Exception ex) {
        return Map.of("error", "internal_error", "message", "Something went wrong on our end.");
    }
}
```

*What just happened:* This one class is now the translation layer for the entire app. Handler 1 turns our
`BookNotFoundException` into a 404 from any controller. Handler 2 catches the `MethodArgumentNotValidException`
that `@Valid` throws and walks `getBindingResult().getFieldErrors()` to build a tidy field→message map - far
cleaner than Spring's noisy default. Handler 3 is the safety net: `Exception.class` catches *everything
else*, so an unexpected bug still produces a deliberate 500 with a generic message instead of leaking internals.

💡 Spring picks the **most specific** matching handler. A `BookNotFoundException` matches both handler 1 and
the `Exception` catch-all in handler 3 - Spring chooses 1 because it's the closest match. That's what lets
the catch-all sit there as a backstop without swallowing the cases you handle precisely.

A validation failure now returns the shape you actually want:

```json
{
  "error": "validation_failed",
  "fields": {
    "title": "title is required",
    "isbn": "isbn must be 13 digits"
  }
}
```

*What just happened:* The `message` strings you wrote on the DTO's constraints back in Phase 6 come straight
back to the client, keyed by field - one consistent shape, produced in one place, for every endpoint that
uses `@Valid`.

## Correct HTTP status codes

The hardest part of error handling usually isn't the wiring - it's choosing the *right* status. The status
code is the first thing a client reads, and machines route on it (retry on 5xx, fix-your-request on 4xx),
so getting it right matters. Here's the working map for an API like ours:

| Status | Meaning | When you return it |
|--------|---------|--------------------|
| **400** Bad Request | The request itself is malformed | Unparseable JSON, a missing required field, wrong type |
| **401** Unauthorized | You're not authenticated | No login / no valid token (covered in [Phase 9](09-security-with-spring-security.md)) |
| **403** Forbidden | Authenticated, but not allowed | Logged in, but lacking permission for this action |
| **404** Not Found | The resource doesn't exist | `GET /api/books/999` where 999 isn't a real id |
| **409** Conflict | The request fights current state | Creating a book whose ISBN already exists (our `DuplicateIsbnException`) |
| **422** Unprocessable | Syntactically fine, semantically invalid | Failed business-rule validation (some teams use this where others use 400) |
| **500** Server Error | *You* broke, not the client | An unexpected exception, a NullPointerException, a DB outage |

📝 The dividing line that resolves most arguments: **4xx means the client's fault** (don't retry the same
thing, fix it), **5xx means the server's fault** (the request was reasonable; something on our end failed).
That's exactly why the default 500 for a missing book was wrong - a missing book is a 4xx situation.

So our `DuplicateIsbnException` from Phase 6 deserves a **409 Conflict**: the request was well-formed, but
it conflicts with a book that already exists. Wire it into the advice class:

```java
@ExceptionHandler(DuplicateIsbnException.class)
@ResponseStatus(HttpStatus.CONFLICT)             // 409
public Map<String, Object> handleDuplicate(DuplicateIsbnException ex) {
    return Map.of("error", "duplicate_isbn", "message", ex.getMessage());
}
```

*What just happened:* A duplicate ISBN now returns 409, which tells the client precisely what kind of
problem this is - a conflict with existing state, not a server failure and not a malformed request. They
know not to blindly retry; they need to change the ISBN.

⚠️ **Never return a 200 with an error inside the body.** It's tempting to send
`{ "success": false, "error": "..." }` with a 200 OK, but it's a trap: every monitoring tool, proxy, retry
policy, and client library reads the *status* to decide if a call succeeded. A 200 says "all good," so a
failure dressed as 200 sails past all of them silently. Put the failure in the status code where the
machinery can see it.

## Consistent error shape with `ProblemDetail`

We've been returning ad-hoc maps - fine, but every handler invents its own keys (`error` here,
`message` there), and clients have to learn each shape. Spring Boot ships a standard you can adopt instead.

📝 **`ProblemDetail`** is Spring's built-in implementation of **RFC 7807** ("Problem Details for HTTP
APIs") - an internet standard for error bodies. It gives every error the same predictable fields: `type`
(a URI identifying the problem kind), `title` (a short human label), `status` (the HTTP code, mirrored into
the body), and `detail` (a human-readable explanation), plus any custom fields you tack on. Use it
everywhere and clients can parse *one* shape, forever.

```java
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BookNotFoundException.class)
    public ProblemDetail handleNotFound(BookNotFoundException ex) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
        pd.setTitle("Book not found");
        pd.setProperty("errorCode", "book_not_found");   // custom field, allowed by the spec
        return pd;
    }

    @ExceptionHandler(DuplicateIsbnException.class)
    public ProblemDetail handleDuplicate(DuplicateIsbnException ex) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
        pd.setTitle("Duplicate ISBN");
        pd.setProperty("errorCode", "duplicate_isbn");
        return pd;
    }
}
```

*What just happened:* `ProblemDetail.forStatusAndDetail` builds the standard body and sets the HTTP status
in one call - no more `@ResponseStatus`, because the status now lives *in* the `ProblemDetail` itself. We
add a `title` and a custom `errorCode` property for clients that want to switch on a stable code. A 404 now
serializes to:

```json
{
  "type": "about:blank",
  "title": "Book not found",
  "status": 404,
  "detail": "No book found with id 999",
  "errorCode": "book_not_found"
}
```

*What just happened:* Every error in your API can now share this exact skeleton - `type`, `title`,
`status`, `detail` - so a client writes its error-parsing code *once*. (`type` defaults to `about:blank`
when you don't supply a problem-specific URI; that's the spec's "nothing fancier than the status" value.)
The content type even comes back as `application/problem+json`, the standard's official media type, so
clients can detect a problem response by its type alone.

💡 **Good error handling is a contract.** A predictable status code plus a predictable body means clients
can build robust handling once and trust it across every endpoint - the same way the validation 400s from
Phase 6 gave a consistent shape for bad input. Switch those to `ProblemDetail` too and your *entire* API
speaks one error language, the foundation the tests in [Phase 8](08-testing-spring-boot.md) assert on.

## Recap

1. **The default is bad.** An uncaught exception becomes a generic **500** with a useless (or
   internals-leaking) body - even when the real problem is a 4xx the client caused. You must translate
   exceptions into correct responses yourself.
2. **`@ExceptionHandler` catches a specific type** and turns it into a chosen status + body. Inside a
   controller it's local to that controller - good for one-offs, duplicative for anything shared.
3. **`@RestControllerAdvice` centralizes handling app-wide.** One class maps not-found → 404, validation →
   400, and a `Exception.class` catch-all → a clean 500. Spring always picks the most specific handler.
4. **Pick the correct status.** 4xx = client's fault (400 bad input, 401/403 auth, 404 missing, 409
   conflict, 422 validation); 5xx = server's fault. ⚠️ Never return 200 with an error body - every tool
   routes on the status code.
5. **`ProblemDetail` gives one consistent shape (RFC 7807):** `type`, `title`, `status`, `detail`, plus
   custom properties, served as `application/problem+json`. Use it everywhere so clients parse errors once.
6. **Error handling is a contract.** Predictable status + predictable body across every endpoint is what
   makes your API trustworthy - connect it to the Phase 6 validation 400s for a single error language
   end to end.

## Quick check

Make sure the translation layer and its status-code rules stuck:

```quiz
[
  {
    "q": "Your service throws an uncaught BookNotFoundException for GET /api/books/999. With no exception handling configured, what does the client receive?",
    "choices": [
      "A 500 Internal Server Error with a generic (or stack-trace-leaking) body, even though the real problem is the client's",
      "A 404 Not Found automatically, because Spring detects the word 'NotFound' in the exception name",
      "An empty 200 OK response",
      "Nothing - the connection hangs until it times out"
    ],
    "answer": 0,
    "explain": "Spring's default for any uncaught exception is 500 Internal Server Error, which falsely blames the server for what is really a client asking for a missing resource. The body is useless and may even leak a stack trace. You have to translate the exception into the correct status (404) yourself."
  },
  {
    "q": "What is the key advantage of @RestControllerAdvice over a per-controller @ExceptionHandler?",
    "choices": [
      "Its @ExceptionHandler methods apply to every controller in the app, so you write the exception-to-response mapping once instead of copying it into each controller",
      "It runs faster because it skips the controller layer entirely",
      "It is the only place @Valid validation can be triggered",
      "It automatically converts every exception into a 200 OK"
    ],
    "answer": 0,
    "explain": "@RestControllerAdvice is a single, app-wide home for exception handling. Handlers inside it apply to all controllers, giving the whole API consistent errors from one class. A per-controller @ExceptionHandler only covers its own controller, so shared cases would have to be copied everywhere."
  },
  {
    "q": "A client tries to create a book whose ISBN already exists. Which HTTP status is the correct choice?",
    "choices": [
      "409 Conflict - the request is well-formed but conflicts with existing state",
      "500 Internal Server Error - any failure is a server error",
      "200 OK with \"success\": false in the body so the client can read the reason",
      "404 Not Found - the new book does not exist yet"
    ],
    "answer": 0,
    "explain": "A duplicate ISBN is a 409 Conflict: the request was valid, but it fights the current state of the data. 500 would falsely blame the server; 404 is for missing resources; and returning 200 with an error body hides the failure from every tool that routes on the status code."
  }
]
```


---

# Testing Spring Boot Apps

Back in [Phase 6](06-service-layer-and-validation.md) you split the app into three layers - controller, service, repository - and the closing line promised the payoff would be testability. This is where you collect on that promise.

**The way you tested plain Java (see [Testing, Build & Profiling](/guides/java-from-zero/16-testing-and-profiling)) still applies - JUnit, AAA, Mockito - but Spring adds a new dimension.** A Spring app isn't just classes; it's classes *wired together by a container*. So you now get a choice for every test: a plain object test with no container at all (fastest), a *slice* that boots one thin layer (fast, focused), or the whole application context booted end to end (slowest, highest confidence)? The skill here isn't any single annotation - it's knowing which to reach for, and why.

Each of those tests is only *possible* because the layers are separable - a controller that talked straight to the database couldn't be slice-tested. The clean seams from Phase 6 are what make everything below work.

## Plain unit test - the service with a mocked repository

📝 The service from Phase 6 had its dependency - the `BookRepository` - *injected through its constructor*. That single fact is what makes it trivially testable: in a test you construct the service yourself and hand it a fake repository. No Spring, no container, no database. This is a plain JUnit + Mockito test, identical in spirit to what you saw in [Mocking & Test Doubles](/guides/mocking-and-test-doubles) - Spring isn't involved at all, which is exactly why it runs in milliseconds.

Here's the service we're testing (trimmed to the duplicate-ISBN rule from Phase 6):

```java
@Service
public class BookService {

    private final BookRepository books;

    public BookService(BookRepository books) {
        this.books = books;
    }

    @Transactional
    public BookResponse create(CreateBookRequest req) {
        if (books.existsByIsbn(req.isbn())) {
            throw new DuplicateIsbnException(req.isbn());
        }
        Book entity = new Book();
        entity.setTitle(req.title());
        entity.setAuthor(req.author());
        entity.setIsbn(req.isbn());
        Book saved = books.save(entity);
        return new BookResponse(saved.getId(), saved.getTitle(),
                                saved.getAuthor(), saved.getIsbn());
    }
}
```

```java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class BookServiceTest {

    @Mock BookRepository books;            // a fake repository we control
    @InjectMocks BookService service;      // Mockito builds the service with the mock

    @Test
    void rejectsDuplicateIsbn() {
        // Arrange: pretend a book with this ISBN already exists
        when(books.existsByIsbn("9780000000001")).thenReturn(true);
        var req = new CreateBookRequest("Dune", "Herbert", "9780000000001");

        // Act + Assert: the service must refuse, and must NOT try to save
        assertThatThrownBy(() -> service.create(req))
            .isInstanceOf(DuplicateIsbnException.class);
        verify(books, never()).save(any());
    }

    @Test
    void savesAndMapsToResponseWhenIsbnIsNew() {
        when(books.existsByIsbn("9780000000002")).thenReturn(false);
        var saved = new Book();            // what the repo "returns" after save
        saved.setId(42L);
        saved.setTitle("Dune");
        saved.setAuthor("Herbert");
        saved.setIsbn("9780000000002");
        when(books.save(any(Book.class))).thenReturn(saved);

        var req = new CreateBookRequest("Dune", "Herbert", "9780000000002");
        BookResponse res = service.create(req);

        assertThat(res.id()).isEqualTo(42L);
        assertThat(res.title()).isEqualTo("Dune");
    }
}
```

*What just happened:* `@ExtendWith(MockitoExtension.class)` turns on Mockito's annotations; `@Mock` builds a fake `BookRepository`, and `@InjectMocks` constructs a real `BookService` with that fake passed into the constructor - the same constructor injection from Phase 6, just driven by the test instead of Spring. The first test programs the mock to claim the ISBN exists, then asserts the service throws *and* never calls `save`. The second programs a fresh-ISBN path and verifies the entity-to-DTO mapping. No `@SpringBootTest`, no application context - pure objects, the fastest test you can write, and it should be the *bulk* of your suite.

💡 If a piece of business logic is awkward to test this way - if you find yourself needing to boot Spring just to exercise a calculation - that's usually a signal the logic is in the wrong layer. Plain-unit-testable services are a side effect of good layering, not a separate goal.

## Slice test: `@WebMvcTest` + MockMvc - just the web layer

The service test above never touched HTTP. But the controller has its own job worth testing: does `@Valid` reject a blank title? Does a `DuplicateIsbnException` come back as the right status? You *could* boot the whole app to check that, but there's a sharper tool.

📝 `@WebMvcTest` is a **slice test**: it starts *only* the web layer - your controller, the JSON serializer, the validation machinery, the exception handlers - and nothing else. No repository, no database, no service beans. Because the controller still needs a `BookService`, you supply a *mocked* one with `@MockitoBean`. Then `MockMvc` fires fake HTTP requests *in-process* and lets you assert on the status code and JSON body - focused entirely on the controller's contract, booting in a fraction of the time a full context does.

```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.junit.jupiter.api.Test;

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(BookController.class)        // boot ONLY this controller's slice
class BookControllerTest {

    @Autowired MockMvc mvc;              // fires fake HTTP requests
    @MockitoBean BookService service;    // the controller's dependency, mocked

    @Test
    void returns201AndBodyForValidRequest() throws Exception {
        when(service.create(any()))
            .thenReturn(new BookResponse(1L, "Dune", "Herbert", "9780000000001"));

        mvc.perform(post("/api/books")
                .contentType("application/json")
                .content("""
                    {"title":"Dune","author":"Herbert","isbn":"9780000000001"}
                    """))
           .andExpect(status().isCreated())
           .andExpect(jsonPath("$.id").value(1))
           .andExpect(jsonPath("$.title").value("Dune"));
    }

    @Test
    void returns400WhenTitleIsBlank() throws Exception {
        mvc.perform(post("/api/books")
                .contentType("application/json")
                .content("""
                    {"title":"","author":"Herbert","isbn":"9780000000001"}
                    """))
           .andExpect(status().isBadRequest());

        verifyNoInteractions(service);   // validation failed before the service ran
    }
}
```

```console
$ mvn -Dtest=BookControllerTest test
[INFO] Running com.example.books.BookControllerTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
```

*What just happened:* `@WebMvcTest(BookController.class)` told Spring to wire up exactly one controller plus the web infrastructure around it - crucially, *not* your service or repository, so there's nothing to talk to a database. `@MockitoBean` puts a Mockito mock of `BookService` into that mini-context. `MockMvc` sends a fake `POST` straight into Spring's request-handling pipeline. The first test asserts the controller returns `201` with the right JSON. The second sends a blank title and asserts a `400` - `verifyNoInteractions(service)` proves `@Valid` rejected the request *before* the controller called the service. (Older Spring Boot spells `@MockitoBean` as `@MockBean` - same idea.)

💡 A `@WebMvcTest` is the right home for everything HTTP-shaped: status codes, JSON structure, validation, and how your `@RestControllerAdvice` from [Phase 7](07-error-handling.md) maps exceptions to responses. Keep the *business* assertions in the plain service test, where they're even cheaper.

## Slice test: `@DataJpaTest` - just the persistence layer

The mirror image of `@WebMvcTest` covers the bottom of the stack.

📝 `@DataJpaTest` boots *only* the JPA slice: your entities, your repositories, and an in-memory database (H2 by default) - and nothing above it. It's how you test that a custom query method actually returns what you think, or that a derived method name like `existsByIsbn` maps to the right SQL. Each test runs in a transaction that's **rolled back at the end**, so tests don't pollute each other - every test starts from a clean slate.

```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

@DataJpaTest                              // boot ONLY the JPA slice + in-memory DB
class BookRepositoryTest {

    @Autowired BookRepository books;     // the real repository, real queries

    @Test
    void existsByIsbnFindsASavedBook() {
        Book b = new Book();
        b.setTitle("Dune");
        b.setAuthor("Herbert");
        b.setIsbn("9780000000001");
        books.save(b);

        assertThat(books.existsByIsbn("9780000000001")).isTrue();
        assertThat(books.existsByIsbn("0000000000000")).isFalse();
    }
}
```

*What just happened:* `@DataJpaTest` spun up an H2 database, created the schema from your `@Entity` mappings, and gave you a *real* `BookRepository` - not a mock. You saved a book and exercised the actual `existsByIsbn` query against a live (if in-memory) database, confirming it returns `true`/`false` correctly. When the test ends, the transaction rolls back and the row vanishes. This is where you catch query bugs a mocked repository can never reveal.

## Full integration: `@SpringBootTest` - the whole app, wired

Slices are deliberately partial. Sometimes you want the opposite: the *entire* application context, every real bean, exercised end to end the way production runs it.

📝 `@SpringBootTest` boots the **full application context** - all your real controllers, services, and repositories, wired together exactly as they are at runtime. Combined with `MockMvc` (or a real HTTP client), one test can drive a request through the controller, into the real service, down to a real repository and database, and back. It's the highest-confidence test you can write, because nothing is faked - and for the same reason it's the slowest, because there's a whole context to build.

```java
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import org.junit.jupiter.api.Test;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest                          // boot the ENTIRE app context
@AutoConfigureMockMvc
class BookApiIntegrationTest {

    @Autowired MockMvc mvc;

    @Test
    void createsThenRejectsDuplicate() throws Exception {
        String body = """
            {"title":"Dune","author":"Herbert","isbn":"9780000000001"}
            """;

        // First create succeeds end-to-end (controller -> service -> real DB)
        mvc.perform(post("/api/books").contentType("application/json").content(body))
           .andExpect(status().isCreated());

        // Second create hits the REAL duplicate-ISBN rule against the REAL row
        mvc.perform(post("/api/books").contentType("application/json").content(body))
           .andExpect(status().isConflict());   // 409 from Phase 7's handler
    }
}
```

*What just happened:* `@SpringBootTest` built the real context - no mocks anywhere - and `@AutoConfigureMockMvc` gave us a `MockMvc` to drive it. The first `POST` flows through the genuine controller, service, and repository, writing to a real (test) database. The second `POST` sends the same ISBN, and the *actual* duplicate-ISBN logic from Phase 6 and the *actual* exception handler from Phase 7 turn it into a `409`. Nothing stubbed - a pass here means the layers genuinely fit together.

### Which test, when - the pyramid

💡 The guiding shape is the **test pyramid** (covered in depth in [Unit, Integration & E2E](/guides/unit-integration-e2e)): *many* fast unit tests at the base, *some* slice tests in the middle, and a *few* full integration tests at the top.

```mermaid
flowchart TD
  A["Few: @SpringBootTest - whole app, slow, highest confidence"] --> B["Some: @WebMvcTest / @DataJpaTest - one slice, fast, focused"]
  B --> C["Many: plain JUnit + Mockito - no Spring, milliseconds"]
```

The reasoning is economic. A full `@SpringBootTest` proves the most but costs the most - boot time per test, and a wide blast radius when it fails (the failure could be anywhere in the app). A plain service test proves one thing but tells you *exactly* where the problem is, instantly. So push every assertion to the lowest level that can hold it: business rules → service unit test; HTTP contract → `@WebMvcTest`; query → `@DataJpaTest`; and reserve `@SpringBootTest` for a handful of "does the whole thing actually fit together" checks. ⚠️ A suite that's all `@SpringBootTest` will be correct and *miserably slow* - slow enough that people stop running it, which defeats the point.

## Realistic databases with Testcontainers

There's a crack in the slice and integration tests above, and it's worth naming plainly.

⚠️ **H2 is not Postgres.** The in-memory database that `@DataJpaTest` and a default `@SpringBootTest` use is convenient, but it is *not* the database you run in production. It has subtly different SQL dialects, different handling of types and constraints, no real `JSONB`, no Postgres-specific functions. A query can pass against H2 and then fail against the real Postgres your app actually talks to - which means your "integration" test gave you false confidence about the one thing integration tests exist to verify.

📝 **Testcontainers** closes that gap. It's a library that, during the test, starts a *real* database (the actual Postgres image) inside a throwaway Docker container, points your app at it, runs the test, and tears the container down afterward. Your integration test now runs against the genuine engine, dialect and all - no more "works on H2, breaks in prod." The cost: it needs Docker and is slower than H2, which is why you use it for a few top-of-pyramid tests, not hundreds of unit tests.

```java
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@SpringBootTest
@Testcontainers
class BookApiWithPostgresTest {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16");   // a REAL Postgres, in Docker

    @DynamicPropertySource
    static void datasource(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    // ... your @Test methods now run against real Postgres ...
}
```

*What just happened:* the `@Container` field declares a real `postgres:16` instance that Testcontainers starts in Docker before the tests run. `@DynamicPropertySource` is the bridge: it reads the container's randomly-assigned URL, username, and password *after* it boots and feeds them into Spring's datasource properties, so the application context connects to that container instead of H2. From there your tests are ordinary `@SpringBootTest` tests - except every query now hits the same database engine production uses. When the suite finishes, the container is destroyed and leaves nothing behind.

💡 Every test in this phase was possible because the layers from [Phase 6](06-service-layer-and-validation.md) could be isolated, faked, or swapped one at a time. Untestable code is almost always *badly layered* code. When a test is hard to write, it's usually telling you the truth about your design.

## Recap

1. **Three tiers of tests, by how much they boot.** Plain JUnit + Mockito (no Spring), slice tests (`@WebMvcTest`, `@DataJpaTest` - one layer), and `@SpringBootTest` (the whole context). Constructor injection from Phase 6 is what makes the plain unit test trivial.
2. **Unit-test the service with a mocked repository.** `@Mock` + `@InjectMocks`, no container - milliseconds per test. This should be the bulk of your suite, and it's where business rules belong.
3. **`@WebMvcTest` + MockMvc tests the web layer alone.** Boots one controller with the service `@MockitoBean`-ed, fires fake HTTP requests, and asserts status codes and JSON - the place for validation and exception-handler tests.
4. **`@DataJpaTest` tests the persistence layer alone.** Real repository against an in-memory DB, each test rolled back; catches query bugs a mocked repository never could.
5. **`@SpringBootTest` boots everything for end-to-end confidence.** Highest confidence, slowest, widest blast radius. ⚠️ Follow the test pyramid - many unit, some slice, few full - or your suite becomes too slow to run.
6. **Testcontainers gives you the real database.** ⚠️ H2 isn't Postgres; for trustworthy integration tests, run a real Postgres in Docker via `@Container` + `@DynamicPropertySource`. 💡 Untestable code is usually badly layered code - the clean seams from Phase 6 are what made all of this possible.

## Quick check

Make sure the three tiers - and when to reach for each - actually stuck:

```quiz
[
  {
    "q": "You want to verify that POST /api/books returns 400 when the title is blank, without booting your repository or database. Which test fits best?",
    "choices": [
      "@WebMvcTest with the service mocked via @MockitoBean, driven by MockMvc",
      "A plain JUnit + Mockito test of BookService",
      "@DataJpaTest against the in-memory database",
      "@SpringBootTest with Testcontainers"
    ],
    "answer": 0,
    "explain": "Validation and HTTP status codes are the web layer's contract. @WebMvcTest boots only the controller plus the validation/serialization machinery, mocks the service with @MockitoBean, and uses MockMvc to fire a fake request and assert the 400 - no repository or database involved, and far faster than a full context."
  },
  {
    "q": "Why is the plain unit test of BookService (with @Mock repository and @InjectMocks) so fast compared to the other styles?",
    "choices": [
      "It doesn't start a Spring application context at all - it just constructs the service with a fake repository, the same constructor injection Spring would use",
      "Spring caches the context so the first run is slow but the rest are instant",
      "Mockito runs tests on multiple threads in parallel automatically",
      "It skips the JIT warmup that slows down @SpringBootTest"
    ],
    "answer": 0,
    "explain": "The service takes its repository through its constructor (Phase 6), so the test builds the service itself and passes in a Mockito mock - no container, no database, no auto-configuration. There's nothing to boot, which is why it runs in milliseconds and forms the base of the pyramid."
  },
  {
    "q": "Your @DataJpaTest passes against the default in-memory H2 database. Why might you still reach for Testcontainers?",
    "choices": [
      "H2 isn't the database you run in production; Testcontainers starts a real Postgres in Docker so dialect and behavior differences can't give you false confidence",
      "Testcontainers makes the tests run faster than H2",
      "@DataJpaTest cannot save entities without Testcontainers",
      "H2 cannot roll back transactions between tests"
    ],
    "answer": 0,
    "explain": "H2 has a different SQL dialect and feature set from Postgres, so a query can pass on H2 and fail in production. Testcontainers spins up the real Postgres image in a throwaway container and points the app at it, so your integration tests exercise the actual engine - at the cost of needing Docker and being slower, which is why you reserve it for a few top-of-pyramid tests."
  }
]
```


---

# Security with Spring Security

Spring Security intimidates almost everyone. People who write JPA mappings and transactional services without breaking a sweat open the security config, see a wall of `.authorizeHttpRequests(...)` and `.hasRole(...)` and something called a "filter chain," and quietly back away. The reputation is earned - for years the API was genuinely awkward, and most tutorials hand you a magic blob of config without explaining the machine underneath.

Before a single line of config, here's the one idea that turns Spring Security from voodoo into something you can reason about:

> **Spring Security is a chain of servlet filters that every request passes through *before* it reaches your controller.**

That's the whole secret. Hold that picture and everything else in this phase falls into place.

## The filter chain - the mental model that fixes everything

📝 A **servlet filter** is a piece of code that wraps every incoming HTTP request *before* your controller runs. Spring Security installs a whole ordered **chain** of them. A request comes in and walks through the chain one filter at a time - each filter asks a single question and either lets the request continue or stops it cold.

The questions look like this, roughly in order:

- *Is this path public (like `/login` or `/public/**`)?* If so, wave it through.
- *Is there a session, a token, or login credentials attached?* If so, figure out **who** this is.
- *Now that we know who they are, are they **allowed** to hit this URL?* If not, reject with 403.

Only if the request survives every filter does it finally reach your `@RestController`. If any filter says no, the request is turned away right there - your controller code never even runs.

```mermaid
flowchart LR
  Req[HTTP request] --> F1[Filter: public path?]
  F1 --> F2[Filter: extract credentials / token]
  F2 --> F3[Filter: authenticate -> who are you?]
  F3 --> F4[Filter: authorize -> are you allowed?]
  F4 -->|passes all| Ctrl[Your @RestController]
  F1 -.reject.-> Deny[401 / 403]
  F3 -.reject.-> Deny
  F4 -.reject.-> Deny
```

*What just happened:* The request runs a gauntlet of filters, each with one job, and only a request that clears all of them reaches your controller. This is why your controller almost never contains security code - by the time a request gets there, the chain has *already* decided it's authenticated and authorized. When you write a `SecurityFilterChain` bean, you're not writing magic; you're configuring *which filters run* and *what they check*. Every confusing config method maps back to one of those filter questions.

## Authentication vs authorization - two different questions

These two words sound alike, get abbreviated to the nearly-identical authN and authZ, and are constantly confused - including in production code that conflates them and ships a security hole. They are *not* the same question, and the filter chain treats them as separate steps for exactly that reason.

📝 **Authentication (authN) = "who are you?"** It's the login step: you prove your identity with a password, a token, an API key. The output is a verified identity (or a rejection). **Authorization (authZ) = "are you allowed to do this?"** It happens *after* authentication, and it checks whether the now-known user has permission for *this specific action* - usually via **roles** (`ADMIN`, `USER`) or finer-grained permissions.

A worked example makes the split obvious: logging into your bank's app is authentication - the app now knows it's you. Being told "you can view your own account but not transfer from someone else's" is authorization. You're fully authenticated the whole time; you're just not authorized for that one action. Mix these up - checking *that* someone is logged in but never *what* they're allowed to do - and any logged-in user can do anything. One of the most common real-world security bugs.

This distinction is foundational enough that it has its own dedicated guide: [Authentication vs Authorization](/guides/auth-vs-authz). For now, the rule to carry forward: **authenticate first (who), authorize second (what), never collapse the two.**

## Configuring access - the SecurityFilterChain bean

Time for config - and now it won't feel like magic, because you know it's just describing filter behavior. Modern Spring Security (6.x) is configured by declaring a `SecurityFilterChain` **bean** (the old `WebSecurityConfigurerAdapter` you'll see in stale tutorials is gone - ignore it). You're handed a builder and you describe which paths are public, which need any logged-in user, and which need a specific role.

```java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**", "/login").permitAll()   // anyone
                .requestMatchers("/api/admin/**").hasRole("ADMIN")      // ADMIN role only
                .anyRequest().authenticated()                          // everything else: must be logged in
            )
            .httpBasic(basic -> {});   // accept HTTP Basic credentials for now

        return http.build();
    }
}
```

*What just happened:* `authorizeHttpRequests` is the **authorization filter** from our diagram, configured rule by rule. Rules are matched **top to bottom, first match wins**: `/public/**` and `/login` are open to everyone, `/api/admin/**` requires the `ADMIN` role, and `anyRequest().authenticated()` says every other path needs a logged-in user. ⚠️ Put specific rules *before* the broad `anyRequest()` catch-all - a catch-all placed too early swallows the rules below it. Also note: `hasRole("ADMIN")` looks for an authority named `ROLE_ADMIN` - Spring adds the `ROLE_` prefix for you, which trips up everyone once.

## Passwords & users - never store plaintext

Authorization rules are useless if anyone can log in as anyone. So the chain needs to *authenticate* - and that means storing and checking passwords. Here is the single most important rule in this entire phase, in bold because people still get it wrong:

📝 **Never store passwords as plaintext. Store a salted hash, and let a `PasswordEncoder` do it.** Spring's standard choice is **BCrypt**: a deliberately slow, salted hashing algorithm. When a user registers you store `encoder.encode(rawPassword)`; when they log in Spring calls `encoder.matches(rawInput, storedHash)`. The original password is never written down anywhere, so a database leak doesn't hand an attacker everyone's credentials. *Why* hashing (not encryption), *why* slow, and *why* salted is its own rich topic - see [How Passwords Are Stored](/guides/how-passwords-are-stored).

To teach the chain *where* your users live, you provide a `UserDetailsService` - a single method that loads a user by username and returns their stored hash plus their roles. Spring's authentication filter calls it, compares the password, and on success records who you are.

```java
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();   // salted, deliberately slow
}

@Bean
public UserDetailsService users(PasswordEncoder encoder) {
    var admin = User.withUsername("admin")
        .password(encoder.encode("s3cret"))   // store the HASH, never the raw value
        .roles("ADMIN")                        // becomes authority ROLE_ADMIN
        .build();
    return new InMemoryUserDetailsManager(admin);
}
```

*What just happened:* The `PasswordEncoder` bean tells Spring to hash with BCrypt everywhere. `UserDetailsService` defines a single user whose password is *stored as a BCrypt hash*, not the raw string - when "admin" logs in, the filter hashes their input and compares hashes. `InMemoryUserDetailsManager` is perfect for a demo; a real app implements `UserDetailsService` to load users from your database via the repository pattern from [Phase 6](06-service-layer-and-validation.md). `.roles("ADMIN")` is what makes the earlier `hasRole("ADMIN")` rule match this user.

A quick word on the two built-in login styles you'll choose between. **HTTP Basic** (`httpBasic`) sends the username and password on *every* request in a header - dead simple, common for machine-to-machine APIs. **Form login** (`formLogin`) shows a login page, authenticates once, and tracks you with a session cookie afterward - the right fit for browser apps with human users.

⚠️ **None of this is safe without HTTPS.** HTTP Basic literally puts your password (base64-encoded, which is *not* encryption) in a header on every request; form login ships a session cookie around. Over plain HTTP, anyone on the network can read both. In production, terminate TLS and serve everything over HTTPS - non-negotiable. If "TLS" is fuzzy, [HTTPS and TLS](/guides/https-and-tls) explains exactly what it protects and how.

## Stateless APIs & JWT - the overview

The session-cookie model assumes the server *remembers* you between requests (it keeps session state in memory). For a REST API serving mobile apps and other services, that's often the wrong shape - you'd rather each request carry its own proof and the server remember nothing.

📝 **A stateless API carries identity on every request instead of relying on a server-side session.** The dominant approach is the **JWT (JSON Web Token)**: at login the server hands the client a signed token encoding who they are and when it expires; the client sends it back in an `Authorization: Bearer <token>` header on every subsequent request. A custom filter (slotted right into the chain you already understand) validates the signature and expiry, and if it checks out, marks the request authenticated - *without any session lookup*.

```http
GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1Ni
```

*What just happened:* The client attaches its token in the `Authorization` header. A JWT-validation filter near the front of the chain reads it, verifies the signature with the server's secret key, checks it hasn't expired, and - if valid - populates the authenticated identity so the authorization rules downstream can do their job. No session, no server-side memory of this user. That's the whole appeal: any server instance can validate the token, so you scale horizontally without sticky sessions.

We're deliberately *not* writing a full JWT implementation here, and you should be wary of any tutorial that casually does. ⚠️ The footguns are real: **where the client stores the token** (`localStorage` is XSS-exposed; `HttpOnly` cookies dodge that but invite CSRF), **expiry and revocation** (a stateless token can't be "logged out" server-side without extra machinery like a denylist or refresh tokens), and the cardinal sin - **rolling your own crypto or token parsing**. Use a vetted, maintained library (`jjwt` or Nimbus) and lean on it.

For finer control than URL rules give you, **method-level security** lets you guard individual methods. Enable it with `@EnableMethodSecurity` and annotate:

```java
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { ... }
```

*What just happened:* `@PreAuthorize` runs its check *before* the method body executes - same authorization question as the URL rules, just expressed at the method instead of the path. It shines when authorization depends on the actual arguments (`@PreAuthorize("#id == authentication.name")` to let users act only on their own data), which URL patterns can't express.

💡 **Security is the worst possible place to be clever.** The framework's defaults - BCrypt, filter chain ordering, CSRF protection, vetted token libraries - encode years of hard-won lessons and patched vulnerabilities. Configure the battle-tested machine; don't reinvent it. The most secure code you'll write here is the code you *didn't* write.

## Recap

1. **The filter chain is the whole mental model.** Spring Security is an ordered chain of servlet filters every request passes *before* reaching your controller; each filter asks one question (public? authenticated? authorized?) and lets the request continue or stops it. Every config method maps back to a filter.
2. **AuthN ≠ authZ.** Authentication is "who are you?" (login); authorization is "are you allowed?" (roles/permissions), and it comes second. Conflating them - checking that someone's logged in but not what they can do - is a classic hole. See [Authentication vs Authorization](/guides/auth-vs-authz).
3. **Configure access with a `SecurityFilterChain` bean.** `permitAll()` for public paths, `authenticated()` for any logged-in user, `hasRole("ADMIN")` for role-gated paths. Rules match top-down, first match wins - put specifics before the `anyRequest()` catch-all.
4. **Never store plaintext passwords.** Use a `PasswordEncoder` (BCrypt: salted and slow), load users via a `UserDetailsService`, and pick HTTP Basic (machine APIs) or form login (browsers). ⚠️ All of it depends on HTTPS in production - see [How Passwords Are Stored](/guides/how-passwords-are-stored) and [HTTPS and TLS](/guides/https-and-tls).
5. **Stateless APIs use JWTs.** The client sends a signed token in `Authorization: Bearer ...` on each request; a filter validates it, no session needed. ⚠️ Mind token storage, expiry/revocation, and never roll your own crypto. `@PreAuthorize` adds method-level checks.
6. **Don't be clever with security.** Lean on the framework's battle-tested defaults; the safest code is the code the framework already wrote and patched for you.

## Quick check

Make sure the one idea that unlocks Spring Security - and its two most-confused concepts - actually stuck:

```quiz
[
  {
    "q": "What is the core mental model for how Spring Security works?",
    "choices": [
      "It's a chain of servlet filters that every request passes through before reaching your controller, each checking one thing",
      "It encrypts your controller methods so only authorized code can call them",
      "It rewrites your @RestController at compile time to add login checks",
      "It runs as a separate server that proxies requests to your application"
    ],
    "answer": 0,
    "explain": "Spring Security installs an ordered chain of servlet filters. A request walks through them one at a time - is this path public? who are you? are you allowed? - and only a request that clears every filter reaches your controller. Every config method maps back to configuring one of those filters."
  },
  {
    "q": "Which statement correctly distinguishes authentication from authorization?",
    "choices": [
      "Authentication is 'who are you?' (login); authorization is 'are you allowed to do this?' (roles/permissions), and it happens after authentication",
      "Authentication is for APIs and authorization is for web pages",
      "They are two names for the same login step and can be used interchangeably",
      "Authorization happens first to decide whether to even bother authenticating"
    ],
    "answer": 0,
    "explain": "Authentication verifies identity (the login step). Authorization, which runs afterward on a now-known user, decides whether that user may perform a specific action via roles or permissions. Collapsing the two - confirming someone is logged in but never checking what they can do - is a common security hole."
  },
  {
    "q": "Why should you never store a user's password as plaintext, and what does a PasswordEncoder like BCrypt do instead?",
    "choices": [
      "It stores a salted, slow hash of the password; on login Spring hashes the input and compares hashes, so a database leak doesn't expose real passwords",
      "It encrypts passwords with a reversible cipher so they can be decrypted when needed",
      "It compresses passwords to save database space",
      "It sends passwords to an external service for verification on every login"
    ],
    "answer": 0,
    "explain": "BCrypt produces a salted, deliberately slow one-way hash. You store the hash, and at login Spring hashes the submitted password and compares hashes - the raw password is never written down. So even a full database leak doesn't hand an attacker usable credentials. (Hashing is one-way, not reversible encryption.)"
  }
]
```


---

# Production: Actuator, Packaging & Deployment

Everything so far has run one way: hit the green arrow in your IDE, the app boots on `localhost:8081`, and you poke it from a browser. That's the inner loop, great for building. But "it runs on my machine" is not a deployment - it's a demo that happens to be on the right machine.

**Shipping a Spring Boot app is turning your source into one self-contained file and putting that file somewhere a server can run it.** No app server to install, no WAR to drop into Tomcat, no fragile setup script. You build a single jar that already *contains* a web server, feed it production config from the outside (the precedence rules from [Phase 4](04-configuration-and-profiles.md)), and run it with `java -jar`. Optionally wrap that jar in a container so the runtime is identical everywhere.

We'll go in the order you'd actually do it: first make the app *observable* (can a load balancer tell it's alive?), then *package* it, then *configure* it for prod, then *containerize* it, then talk about where it lands.

## Spring Boot Actuator - production endpoints for free

Before you deploy anything, you need to answer a deceptively important question: *how does the outside world know your app is healthy?* A load balancer or Kubernetes needs to ping something and get a yes/no. You could write that endpoint yourself, but Spring Boot already has it - plus metrics, build info, and more - in a module called **Actuator**.

📝 **Actuator** - a Spring Boot starter that adds a set of ready-made HTTP endpoints for monitoring and managing a running app: `/actuator/health` (is it alive and are its dependencies OK?), `/actuator/metrics` (memory, request counts, GC, and more), `/actuator/info` (build/version info you supply). You add one dependency; the endpoints appear.

You add it the same way you add any dependency (recall coordinates from the [Java tooling phase](/guides/java-from-zero) - `groupId:artifactId:version`):

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
```

*What just happened:* You pulled in the actuator starter. Note there's no `<version>` - Spring Boot's parent POM manages versions for its own starters, so they stay in lockstep with your Boot version. On the next restart, Actuator auto-configures itself and registers its endpoints. You wrote zero endpoint code.

The one endpoint you'll use constantly is health. Hit it:

```bash
curl http://localhost:8081/actuator/health
```

```json
{ "status": "UP" }
```

*What just happened:* Actuator reports the app is `UP`. That tiny response is exactly what a load balancer or container orchestrator polls - `UP` (200) keeps traffic flowing; `DOWN` (503) or no answer stops routing and may trigger a restart. The health check is also smart: it aggregates the health of things your app depends on, so a dead database connection flips it to `DOWN` automatically - adding the JPA datasource ([Phase 5](05-persistence-with-jpa.md)) registered that contributor for you.

By default, only `health` is exposed over HTTP. The rest are switched off until you opt in - and that default is a *security feature*, not an oversight:

```yaml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
```

*What just happened:* You explicitly listed which actuator endpoints are reachable over HTTP - `health`, `info`, and `metrics`. Anything not in the list stays unreachable from the outside.

⚠️ **Never expose actuator endpoints carelessly in production.** Some leak serious internals. `/actuator/env` dumps your full configuration - *including resolved property values that may contain secrets*. `/actuator/heapdump` downloads a snapshot of your app's memory. `/actuator/shutdown` can stop the app. Do **not** write `include: "*"` in production. Expose the minimal set you need, and ideally put management endpoints behind authentication or on a separate, internal-only port. Treat the actuator surface as part of your attack surface.

💡 Actuator is your app's *first* observability - health and a handful of metrics out of the box. Real production systems build on this with proper metrics collection, dashboards, and tracing. Actuator exposes metrics in a format tools like Prometheus scrape directly. When you're ready to go deeper than "is it up?", see [/guides/observability-logs-metrics-traces](/guides/observability-logs-metrics-traces).

## Packaging: the fat jar

Now the app knows how to report its health. Time to turn it into something you can copy to a server. In old-school Java you'd build a WAR file and deploy it *into* a separately-installed application server (Tomcat, JBoss). Spring Boot threw that model out. Your build produces one runnable file with the server *inside* it.

📝 **Fat jar (a.k.a. uber jar)** - a single executable `.jar` that bundles three things: your compiled code, every dependency it needs, *and* an embedded web server (Tomcat by default). It has no external requirements beyond a Java runtime. That one file is your entire deployable.

You build it with one command. If you used Spring Initializr, your project came with the Maven wrapper (`mvnw`), so you don't even need Maven installed:

```bash
./mvnw clean package
```

```console
[INFO] --- spring-boot-maven-plugin:3.x.x:repackage ---
[INFO] Replacing main artifact with repackaged archive
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  18.421 s
```

*What just happened:* `clean` wiped the previous build output, then `package` compiled your code, ran your tests, and assembled the jar. The key line is `spring-boot-maven-plugin:repackage` - the plugin (included automatically in an Initializr project) rewriting the plain jar into a *fat* jar with the server and all dependencies tucked inside. The result lands in `target/`, named like `bookstore-0.0.1-SNAPSHOT.jar`.

Now run it the way a server would - no IDE, just Java:

```bash
java -jar target/bookstore-0.0.1-SNAPSHOT.jar
```

```console
 :: Spring Boot ::                (v3.x.x)
... : Starting BookstoreApplication using Java 21
... : Tomcat initialized with port 8081 (http)
... : Started BookstoreApplication in 2.3 seconds
```

*What just happened:* `java -jar` launched the fat jar. Notice Tomcat starts up *from inside the jar* - there's no separate server to install or configure, because it shipped with your code. This is the whole deployment story in one line: build the jar, copy it to a machine that has Java, run `java -jar`. That's it. (Gradle users get the same artifact from `./gradlew bootJar`.)

## Prod configuration: don't ship your dev defaults

The jar runs. But right now it's still wearing its development clothes - an in-memory H2 database, chatty DEBUG logging, maybe wide-open CORS. Shipping those to production ranges from embarrassing to dangerous. This is exactly what profiles and externalized config from [Phase 4](04-configuration-and-profiles.md) are for.

The pattern: keep dev-friendly defaults in your base/dev config, put the real production values in `application-prod.yml`, and feed secrets from the environment. Activate the prod profile when you run:

```bash
SPRING_PROFILES_ACTIVE=prod \
SPRING_DATASOURCE_URL=jdbc:postgresql://db.internal:5432/bookstore \
SPRING_DATASOURCE_USERNAME=bookstore_app \
DB_PASSWORD=$REAL_SECRET \
java -jar target/bookstore-0.0.1-SNAPSHOT.jar
```

*What just happened:* `SPRING_PROFILES_ACTIVE=prod` told Boot to layer `application-prod.yml` over the base config, swapping H2 for the real PostgreSQL and turning logging down to `WARN`. The datasource URL, username, and password came in as environment variables, which sit *above* the config file in precedence - so the real database and the real secret never had to be written into a committed file. Same jar you built a minute ago; production behavior, driven entirely by inputs.

⚠️ **Audit what leaks from dev to prod before you ship.** The usual offenders: the H2 in-memory database (data vanishes on restart), `spring.jpa.hibernate.ddl-auto=create` (drops and recreates your schema - catastrophic against a real DB), DEBUG logging (slow, noisy, and can log sensitive request data), and permissive CORS like `allowedOrigins("*")`. Make `application-prod.yml` an explicit, conservative override of every one - the defaults are tuned for your laptop, not the internet.

## Docker: make the runtime identical everywhere

`java -jar` on a server works, but it quietly assumes that server has the *right* Java version installed and configured. Multiply that across your laptop, a teammate's laptop, CI, staging, and prod, and "works on mine" creeps back in through version drift. A container freezes the entire runtime - the exact JDK, the OS libraries, your jar - into one image that runs identically wherever Docker runs.

A Spring Boot fat jar makes the Dockerfile almost trivial: start from a base image that has Java, copy the jar in, run it.

```dockerfile
# Start from a small image that already has a Java runtime
FROM eclipse-temurin:21-jre

# Where the app lives inside the container
WORKDIR /app

# Copy the fat jar built by `./mvnw package` into the image
COPY target/bookstore-0.0.1-SNAPSHOT.jar app.jar

# Document the port the app listens on
EXPOSE 8081

# The command that runs when the container starts
ENTRYPOINT ["java", "-jar", "app.jar"]
```

*What just happened:* `FROM eclipse-temurin:21-jre` picks a base with a Java 21 *runtime* (a JRE, not the full JDK - smaller, since you only need to *run* the jar). `COPY` drops your fat jar in as `app.jar`. `EXPOSE` is documentation. `ENTRYPOINT` is the command Docker runs on startup - the same `java -jar` you ran by hand, baked into the image. Because the fat jar already bundles Tomcat and every dependency, the Dockerfile needs nothing else.

Build the image and run it:

```bash
docker build -t bookstore:1.0 .
docker run -p 8081:8081 -e SPRING_PROFILES_ACTIVE=prod bookstore:1.0
```

*What just happened:* `docker build` turned the Dockerfile into a tagged image, `bookstore:1.0`. `docker run` started a container from it: `-p 8081:8081` maps the container's port to your host so you can reach it, and `-e SPRING_PROFILES_ACTIVE=prod` passes the active profile in as an environment variable - the *same* externalized-config mechanism, now flowing through Docker. That image is now a portable unit: it runs bit-for-bit identically on your machine, CI, and the production cluster, because the Java version and OS came along inside it.

💡 If Docker itself is still fuzzy - images vs containers, layers, why any of this is an improvement - read [/guides/docker-without-the-magic](/guides/docker-without-the-magic). For Spring Boot specifically, the win is that the fat jar and the container are a natural pair: the jar makes the app self-contained, the image makes the *runtime* self-contained.

## Where it actually runs - and why this is easy

You have a jar, or an image. Where does it go? You've got a spectrum, roughly from most-hands-on to least:

- **A plain VPS or VM.** Copy the jar up, run `java -jar` (usually managed by `systemd` so it restarts on crash/reboot), and put **nginx** in front as a reverse proxy to handle TLS and forward traffic to your app on `localhost:8081`. Maximum control, maximum manual work.
- **A container platform.** Push your image to a registry and let something (Kubernetes, ECS, Cloud Run) schedule and run it. This is where your `/actuator/health` endpoint earns its keep: the platform polls it for *liveness* and *readiness* probes, and that's what enables **zero-downtime rollouts** - it starts new instances, waits until their health says `UP`, shifts traffic over, then retires the old ones. No health endpoint, no safe rollout.
- **A PaaS.** Platforms like Railway, Render, Fly.io, or Heroku take your repo or image and handle the server, TLS, and scaling for you. Least control, least to manage - often the right call for a side project.

💡 The embedded server plus the fat jar is the entire reason Spring Boot deploys so much more easily than classic Java. The old model meant installing and tuning an application server, then deploying a WAR *into* it. Boot collapsed that: the server lives inside your one runnable artifact, so "deploy" becomes "run a file" (or "run an image").

For taking a real project the last mile to a live URL - domain, TLS, picking a host, the unglamorous final 20% - see [/guides/ship-your-side-project](/guides/ship-your-side-project).

## Recap

1. **Actuator** gives you production monitoring endpoints for free - add `spring-boot-starter-actuator` and you get `/actuator/health` (what load balancers and orchestrators poll), `/actuator/metrics`, and `/actuator/info`.
2. **Lock down the actuator surface.** Only `health` is exposed by default; opt others in explicitly with `management.endpoints.web.exposure.include`, and never expose everything - `/actuator/env` and `/actuator/heapdump` can leak secrets.
3. **`./mvnw package` builds a fat jar** - your code, all dependencies, and an embedded Tomcat in one self-contained file. Run it anywhere with `java -jar app.jar`. That file *is* your deployable.
4. **Configure prod from the outside.** Use the `prod` profile plus environment variables for the real database and secrets; never let dev defaults (H2, `ddl-auto=create`, DEBUG logging, wide-open CORS) reach production.
5. **Docker freezes the runtime.** A minimal Dockerfile (JRE base + the jar) produces an image that runs identically everywhere; pass config in with `-e`.
6. **Where it runs** ranges from a VPS behind nginx, to a container platform (where health checks enable zero-downtime rollouts), to a PaaS. The embedded server + fat jar is *why* Spring Boot deploys so easily compared to old WAR-on-Tomcat Java.

## Quick check

Make sure the production picture is solid before the guide wraps up:

```quiz
[
  {
    "q": "Why do load balancers and orchestrators care about /actuator/health?",
    "choices": [
      "It returns a simple UP/DOWN status they poll to decide whether to route traffic to an instance - and it enables zero-downtime rollouts",
      "It speeds up the application by caching responses",
      "It is required for the embedded Tomcat server to start at all",
      "It encrypts traffic between the app and the load balancer"
    ],
    "answer": 0,
    "explain": "Health is the endpoint infrastructure polls. UP (200) means keep sending traffic; DOWN (503) or no answer means stop routing and maybe restart. A container platform uses it for liveness/readiness probes, which is exactly what makes safe, zero-downtime rollouts possible."
  },
  {
    "q": "What makes a Spring Boot fat jar runnable on any machine with just `java -jar app.jar`?",
    "choices": [
      "It bundles your compiled code, all dependencies, AND an embedded web server inside one self-contained file",
      "It compiles your code to native machine code so no JVM is needed",
      "It downloads its dependencies from Maven Central at startup",
      "It includes a copy of the operating system"
    ],
    "answer": 0,
    "explain": "The fat (uber) jar packs your classes, every dependency, and an embedded Tomcat into a single file. There's nothing to install on the target beyond a Java runtime - which is why deployment collapses to copying one file and running it, unlike the old WAR-into-a-server model."
  },
  {
    "q": "Which actuator configuration is dangerous to use in production?",
    "choices": [
      "exposure.include: \"*\" - exposing every endpoint, including /env and /heapdump which can leak secrets and memory contents",
      "exposure.include: health - exposing only the health endpoint",
      "Putting management endpoints behind authentication",
      "Running management endpoints on a separate internal-only port"
    ],
    "answer": 0,
    "explain": "Exposing everything makes endpoints like /actuator/env (resolved config, possibly with secrets), /actuator/heapdump (a full memory snapshot), and /actuator/shutdown reachable. Expose only the minimal set you need, and ideally guard the management surface with auth or a separate port."
  }
]
```


---

# Where to Go Next

Look at what you can actually do now. You can stand up a REST API with real controllers, wire its pieces together with dependency injection instead of `new` everywhere, persist data through Spring Data JPA, separate your service layer from your web layer, validate input, return correct HTTP status codes, write tests that boot the context or slice into one layer, lock endpoints down with Spring Security, and package the whole thing into a runnable JAR with health checks and metrics that you can ship. That is not a toy - it's the shape of the work that pays Spring developers.

This last phase isn't more annotations. It's the map of where the road forks from here, a clear word on each branch, and the one thing that turns this from *read* into *yours*: building something and finishing it. The Spring world is enormous, and it's tempting to feel you have to swallow all of it. You don't. Pick the branch your target job actually uses, go deep there, and leave the rest as names you'd recognize.

## The branches from here

```mermaid
flowchart TD
  You[You: layered, tested, secured API] --> MS[Microservices: Spring Cloud]
  You --> RX[Reactive: WebFlux]
  You --> MSG[Messaging: Kafka / RabbitMQ]
  You --> DATA[Spring Data variants]
  You --> GQL[GraphQL with Spring]
```

*What this shows:* five directions lead out from where you stand. None of them is a do-over - every one builds on the controllers, beans, and config you already understand. The plain advice is the same as it was for picking a project: go deep on one, and let your target job decide which.

## Microservices with Spring Cloud - the enterprise direction

The monolith you can build today is a single deployable. Lots of large organizations instead run dozens of small services that talk to each other, and **Spring Cloud** is the toolkit for the plumbing that makes that bearable: a **config server** so configuration lives in one place instead of scattered across services, **service discovery** so services find each other without hard-coded addresses, and an **API gateway** as the single front door that routes and secures traffic.

This is where a great deal of enterprise Java lives, so if you're aiming at a big shop, it's high-leverage. Look plainly at the trade: microservices buy you independent deployment and scaling at the cost of real operational complexity - network calls that fail, distributed tracing, data spread across services. Worth learning; not worth reaching for on day one of a project a single well-built service would handle.

## Reactive with WebFlux - non-blocking for high concurrency

Everything you built so far is the classic **Spring MVC** model: one thread per request, and that thread waits while the database answers. That's simple to reason about and it's the right default for most applications. **Spring WebFlux** is the other model - **non-blocking**, where a small pool of threads juggles many in-flight requests instead of parking on each one. It shines when you have huge numbers of concurrent connections that spend most of their time waiting (think streaming, or a gateway fanning out to many slow downstreams).

The plain caveat: reactive code is a genuinely different way of thinking (you compose `Mono` and `Flux` pipelines rather than writing straight-line code), and it's harder to debug. Reach for it when you have a concurrency problem that blocking can't solve - not because "non-blocking" sounds faster.

## Messaging - Kafka and RabbitMQ for event-driven systems

So far your services talk by one calling another and waiting for the answer. **Messaging** flips that: a service drops an event onto a queue or topic and moves on, and other services consume it whenever they're ready. This is how you build **asynchronous, event-driven** systems that stay responsive and decoupled - the order service announces "order placed" and doesn't care who's listening. **RabbitMQ** is a classic message broker; **Apache Kafka** is the de facto event-streaming backbone of modern systems. Spring Boot has first-class starters for both.

💡 This is one of the most useful patterns to have in your pocket, and it's worth understanding the concepts *before* the Spring specifics - what a queue is, what delivery guarantees mean, why async changes how you reason about failure. The [Webhooks & Message Queues](/guides/webhooks-and-message-queues) guide is the mental model; the Spring starters are how you wire it up once you have it.

## Spring Data variants - beyond the relational database

You learned Spring Data JPA against a relational database, but the same repository idea stretches across very different stores. **Spring Data MongoDB** gives you the familiar repository abstraction over a document database when your data is more naturally nested than tabular. **Spring Data Redis** puts an in-memory store in reach for caching, sessions, and fast counters. The pleasant surprise here is how little is new: once you understand repositories and how Spring Data derives queries, picking up a new backing store is mostly learning *that store's* trade-offs, not a new framework.

## GraphQL with Spring - a different API shape

REST isn't the only way to expose an API. **GraphQL** lets the client ask for exactly the fields it wants in one request, which can be a real win for rich front-ends that would otherwise make many REST calls. **Spring for GraphQL** is the official integration, and it slots into the same controller-and-service thinking you already have - you're defining a schema and resolvers instead of endpoints. A good branch to know exists; whether it's worth depth depends entirely on whether the teams you want to join use it.

## Demystify further - learn core Spring

💡 Here's the move that truly kills the remaining magic. Spring Boot is **auto-configured Spring** - under every starter and every "it just works" is plain Spring Framework that Boot generated for you. To make the fog lift completely, go *down* a layer and learn the **core Spring Framework**: writing `@Configuration` classes by hand, watching the **bean lifecycle** happen on purpose, seeing the **servlet layer** Boot's embedded server quietly stands up. Do this *after* this guide, not instead of it - Boot is how the job actually gets done.

The [Spring Framework (core)](/guides/spring-framework-from-zero) guide is exactly that demystifier: the "less-magic Spring," where you write the configuration Boot auto-generates. And if you want to zoom out further - why a framework like Spring exists at all, what problem it's solving by taking control away from your `main` method - [What a Framework Even Is](/guides/what-a-framework-even-is) is the mental model the whole stack rests on.

## What to actually build

Reading got you here. *Building* is what makes it stick - and the trick is something small enough to finish but real enough to teach you the messy parts. Three straightforward suggestions, roughly in order:

- **A full CRUD API with auth, a real database, and tests.** Endpoints to create, read, list, update, and delete; persistence through Spring Data JPA; login secured with Spring Security; a handful of tests that prove it works. This one consolidates the *entire* guide into a single thing you can point at and say "I built that." Start here.
- **A second service that calls the first.** Once one service works, stand up another that talks to it over HTTP. You'll feel the first real taste of microservices - service-to-service calls, what happens when the other side is down - without the full Spring Cloud apparatus.
- **Add a message queue between them.** Replace one of those direct calls with an event on RabbitMQ or Kafka, so the first service announces something and the second reacts on its own time. Now you've felt synchronous *and* asynchronous communication, which is most of what distributed systems are.

Whatever you pick, the real instruction is one word: **finish**. One rough project carried all the way to "it runs and I can show it" teaches you more than three polished half-builds abandoned at 80%. Choose the one that excites you and take it end to end, even if it's small.

## A last word, and what to read

Two resources are worth a permanent bookmark: the **official Spring guides at spring.io/guides** - short, focused, task-shaped walkthroughs maintained by the people who build Spring - and the **reference documentation** behind them, genuinely good when you need the real answer instead of a forum guess. When something behaves strangely, the reference docs almost always explain *why*.

Remember the through-line of this whole guide: the magic was never magic. It was layers - a servlet container, a bean container wiring your objects, auto-configuration making sensible defaults, starters pulling in coherent sets of dependencies. You can see every one of those layers now. Go build the small thing, finish it, and ship it. You're ready.

## Recap

1. **You can build and ship a real Spring app** - a layered, tested, secured REST API in a runnable JAR. That's the shape of the work Spring developers are hired to do.
2. **The branches from here:** Spring Cloud (microservices), WebFlux (reactive/non-blocking), messaging with Kafka or RabbitMQ (event-driven), Spring Data variants (MongoDB, Redis), and GraphQL - go deep on the one your target job uses.
3. **Each branch builds on what you know** - controllers, beans, repositories, and config carry over; none of these is starting from scratch.
4. **To truly kill the magic, learn core Spring** - `@Configuration` by hand, the bean lifecycle, the servlet layer - *after* this guide, via [Spring Framework (core)](/guides/spring-framework-from-zero) and [What a Framework Even Is](/guides/what-a-framework-even-is).
5. **Build one real thing and finish it** - a full CRUD API with auth, a database, and tests; then a second service; then a message queue between them. Finishing beats polishing.
6. **Next reading:** the official guides and reference docs at spring.io. The magic was never magic - it's the layers you now understand.

## Quick check

Test yourself on the decisions that matter most as you leave this guide:

```quiz
[
  {
    "q": "What is the real reason to reach for Spring WebFlux over the classic Spring MVC model?",
    "choices": [
      "You have a real high-concurrency problem where many requests spend most of their time waiting",
      "Reactive code is simpler to read and debug than straight-line code",
      "Spring MVC is deprecated and WebFlux is its required replacement",
      "Non-blocking always makes every application faster"
    ],
    "answer": 0,
    "explain": "WebFlux is non-blocking and shines under large numbers of concurrent, mostly-waiting connections. It's a genuinely different (and harder to debug) way of thinking, so reach for it when blocking can't solve your concurrency problem - not because 'non-blocking' sounds faster."
  },
  {
    "q": "What is the relationship between Spring Boot and the core Spring Framework?",
    "choices": [
      "Spring Boot is auto-configured Spring - under every starter is plain Spring Framework it generated for you",
      "Spring Boot replaced the Spring Framework, which no longer exists",
      "They are unrelated frameworks that happen to share a name",
      "Core Spring is a newer rewrite that sits on top of Spring Boot"
    ],
    "answer": 0,
    "explain": "Boot is auto-configured Spring. Learning the core framework - manual @Configuration, the bean lifecycle, the servlet layer - is the way to see what Boot automates, best done after this guide rather than instead of it."
  },
  {
    "q": "What's the most important rule when choosing what to build next?",
    "choices": [
      "Pick one small-but-real project and finish it end to end",
      "Start a microservices system so you cover the most ground at once",
      "Only build something that uses all five branches together",
      "Avoid auth and databases until you've read the full reference docs"
    ],
    "answer": 0,
    "explain": "One rough project finished teaches more than three polished half-builds abandoned at 80%. Start with a full CRUD API with auth, a database, and tests - it consolidates the whole guide - and take it all the way to shipping."
  }
]
```
