August 30, 2026 58 minutes minutes read Admin

Spring Boot - Building Production-Ready Java Applications

The Problem: Building a Real Java Application

Writing a Java program is relatively easy.

Building a real application is a different problem.

A production application usually needs to:

  • expose HTTP APIs

  • authenticate users

  • authorize access

  • validate incoming data

  • communicate with databases

  • manage transactions

  • handle failures

  • return consistent errors

  • log important events

  • collect metrics

  • trace requests across services

  • load configuration

  • manage dependencies

  • start and stop cleanly

  • expose health information

Without a framework, we would have to build much of this infrastructure ourselves.

Imagine starting with:

HTTP Request
     |
     v
HTTP Server
     |
     v
Routing
     |
     v
Authentication
     |
     v
Authorization
     |
     v
Validation
     |
     v
Business Logic
     |
     v
Database
     |
     v
Transaction
     |
     v
Response

And then we also need:

Logging
Metrics
Tracing
Configuration
Error Handling
Security
Connection Pools
Serialization
Dependency Management
Health Checks

This is the problem Spring solves.

And Spring Boot takes that further by making it practical to create and operate Spring applications with sensible defaults.


What Is Spring?

Before understanding Spring Boot, we need to understand Spring itself.

Spring is fundamentally an application framework built around several important ideas.

The most important one is:

Let the framework manage the infrastructure so your application can focus on business logic.

Consider:

public class OrderService {

    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public Order createOrder(Order order) {
        return repository.save(order);
    }
}

The class does not create the repository.

It receives it.

Something else creates the object and provides the dependency.

This is:

Dependency Injection

And dependency injection is part of a larger concept:

Inversion of Control

Instead of your application controlling the creation and lifecycle of everything:

Application
    |
    +-- create Repository
    +-- create Service
    +-- create Controller
    +-- create Database Connection

Spring manages those objects:

                Spring Container
                      |
        +-------------+-------------+
        |             |             |
        v             v             v
   Repository      Service      Controller

The Spring container is responsible for creating, configuring, and connecting application objects.

These managed objects are called:

Beans

What Is Spring Boot?

Spring Boot is built on top of Spring.

It provides conventions, auto-configuration, dependency management, embedded servers, production features, and tooling that make Spring applications much easier to build and run.

Without Spring Boot, configuring a Spring application could require significant manual configuration.

Spring Boot tries to turn:

Many configuration decisions
        |
        v
Sensible defaults
        |
        v
Working application

For example, adding:

spring-boot-starter-web

brings together the dependencies needed for a typical web application.

Spring Boot also detects what is present on the classpath and configures appropriate infrastructure automatically.

This is called:

Auto-Configuration

Spring Boot Application

A minimal Spring Boot application looks like this:

@SpringBootApplication
public class Application {

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

The important annotation is:

@SpringBootApplication

It combines several important capabilities.

Conceptually:

@SpringBootApplication
        |
        +-- @SpringBootConfiguration
        |
        +-- @EnableAutoConfiguration
        |
        +-- @ComponentScan

The important part is understanding what these mean.


Component Scanning

Spring can discover classes annotated with things such as:

@Component
@Service
@Repository
@Controller
@RestController
@Configuration

For example:

@Service
public class UserService {
}

Spring discovers the class and registers it as a bean.

Then another bean can depend on it:

@RestController
public class UserController {

    private final UserService userService;

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

The controller does not create UserService.

Spring connects them:

Spring Container

UserController
      |
      | injected
      v
UserService

This is one of the fundamental mental models of Spring.


Auto-Configuration

Suppose your application contains:

spring-boot-starter-data-jpa

and a database driver.

Spring Boot can configure many pieces of the JPA infrastructure automatically.

Conceptually:

Classpath
   |
   +-- Spring Data JPA
   |
   +-- Hibernate
   |
   +-- Database Driver
   |
   v
Spring Boot
   |
   v
Auto Configuration
   |
   +-- DataSource
   +-- EntityManager
   +-- Transaction infrastructure
   +-- Repository infrastructure

This does not mean Spring Boot magically understands your business requirements.

It means it can configure common infrastructure based on what you have declared.

That distinction is important.

Auto-configuration configures infrastructure. It does not design your application.


Starters

Spring Boot uses starters to provide convenient dependency groups.

For example:

spring-boot-starter-web
spring-boot-starter-security
spring-boot-starter-data-jpa
spring-boot-starter-validation
spring-boot-starter-actuator

Instead of manually selecting many transitive dependencies, you choose the capability you need.

For example:

REST API
    |
    v
spring-boot-starter-web

or:

Database
    |
    v
spring-boot-starter-data-jpa

This reduces dependency-management complexity.


Configuration

Applications need configuration.

Examples:

Database URL
Database username
Database password
Server port
Logging level
External service URL
Feature flags

Spring Boot supports configuration through properties or YAML.

For example:

server:
  port: 8080

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/orders
    username: orders
    password: secret

A major principle is:

Configuration should not be hard-coded into application logic.

Instead:

Application
     |
     v
Configuration
     |
     +-- Development
     +-- Testing
     +-- Production

The same application can therefore run in different environments.


Profiles

Spring profiles allow different configuration for different environments.

For example:

application.yml
application-dev.yml
application-test.yml
application-prod.yml

You might have:

spring:
  profiles:
    active: prod

Then Spring Boot can load the appropriate configuration.

The goal is:

Same Application
       |
       +-- dev
       +-- test
       +-- staging
       +-- production

without changing application code.


Spring Web

Now we can look at the web layer.

Spring Web provides the infrastructure needed to build HTTP applications.

A typical request looks like:

HTTP Request
     |
     v
Web Server
     |
     v
Spring MVC
     |
     v
Controller
     |
     v
Service
     |
     v
Repository
     |
     v
Database

A controller might look like:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public UserResponse getUser(@PathVariable Long id) {
        return userService.getUser(id);
    }
}

The framework handles:

  • HTTP routing

  • request parsing

  • parameter binding

  • JSON serialization

  • response generation

  • filters

  • interceptors

The controller can therefore focus on translating HTTP requests into application operations.


Spring MVC

The traditional Spring Web stack is based on Spring MVC.

MVC stands for:

Model
View
Controller

For REST APIs, the view is usually not an HTML page.

Instead, the controller returns data.

For example:

@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
    return service.findUser(id);
}

Spring can serialize:

UserResponse

into:

{
  "id": 42,
  "name": "Alice"
}

This is handled by HTTP message converters, commonly using Jackson for JSON.


Controller vs Service vs Repository

One of the most useful Spring application structures is:

Controller
    |
    v
Service
    |
    v
Repository
    |
    v
Database

Each layer has a different responsibility.

Controller

Responsible for HTTP.

HTTP
Request
Response
Status Code
Headers

Service

Responsible for business logic.

Business Rules
Transactions
Use Cases
Coordination

Repository

Responsible for data access.

Database
Queries
Persistence

For example:

@RestController
class OrderController {

    private final OrderService service;

    @PostMapping
    OrderResponse create(@RequestBody CreateOrderRequest request) {
        return service.createOrder(request);
    }
}

Then:

@Service
class OrderService {

    private final OrderRepository repository;

    @Transactional
    public OrderResponse createOrder(CreateOrderRequest request) {

        Order order = new Order(request.customerId());

        repository.save(order);

        return OrderResponse.from(order);
    }
}

And:

public interface OrderRepository
        extends JpaRepository<Order, Long> {
}

The separation makes the application easier to reason about.


Spring Security

A web application must answer two different questions:

Who are you?

and:

What are you allowed to do?

These are:

Authentication
Authorization

Authentication establishes identity.

Authorization determines permissions.

For example:

Request
   |
   v
Authentication
   |
   v
User = alice
   |
   v
Authorization
   |
   +-- READ_USERS?  YES
   +-- DELETE_USERS? NO

Spring Security provides infrastructure for this.

When Spring Security is present, Spring Boot applies default web security unless you provide your own security configuration.


Security Filter Chain

One of the most important concepts in Spring Security is the filter chain.

Conceptually:

HTTP Request
     |
     v
Security Filter Chain
     |
     +-- Authentication
     |
     +-- Authorization
     |
     +-- CSRF
     |
     +-- Security Context
     |
     v
Controller

The request does not simply go directly to the controller.

Security gets an opportunity to inspect it first.


Authentication

Suppose the client sends:

Authorization: Bearer <token>

A security component can:

Extract token
     |
     v
Validate token
     |
     v
Determine identity
     |
     v
Create Authentication
     |
     v
SecurityContext

Then application code can access the authenticated identity.


Authorization

Authentication answers:

Who is this?

Authorization answers:

Can this user perform this operation?

For example:

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

Now only users with the required authority can invoke the operation.

Method-level security can be enabled with:

@EnableMethodSecurity

Spring Security provides both request-level and method-level authorization mechanisms.


Security Is Not Just Authentication

A common mistake is to think:

JWT = Security

It is not.

A production security design also needs to consider:

Authentication
Authorization
Password Storage
Session Management
CSRF
CORS
Input Validation
Rate Limiting
Secrets
TLS
Security Headers
Audit Logging

Security is a system property, not simply an annotation.


Spring Data

Most applications need persistent data.

Without an abstraction, application code might contain large amounts of:

Connection
PreparedStatement
ResultSet

Spring Data provides abstractions that make data access much simpler.

One of the most popular modules is:

Spring Data JPA

For example:

public interface UserRepository
        extends JpaRepository<User, Long> {
}

Now you get operations such as:

save()
findById()
findAll()
deleteById()
existsById()

without writing all the implementation code yourself.


Repository Abstraction

Suppose we have:

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
}

Then:

public interface UserRepository
        extends JpaRepository<User, Long> {
}

Spring Data creates the repository implementation.

Conceptually:

Your Interface
     |
     v
Spring Data
     |
     v
Repository Implementation
     |
     v
JPA / Hibernate
     |
     v
JDBC
     |
     v
Database

This is another example of Spring managing infrastructure.


Query Methods

Spring Data can derive queries from method names.

For example:

List<User> findByName(String name);

or:

Optional<User> findByEmail(String email);

The method name expresses the query intention.

You can also define explicit queries:

@Query("""
    select u
    from User u
    where u.email = :email
""")
Optional<User> findUserByEmail(String email);

This is convenient, but there is an important warning.

Do not let repository abstractions hide the database.

You still need to understand:

Indexes
Joins
Query Plans
N+1 Queries
Transactions
Connection Pools
Locking
Pagination

Spring Data makes database access easier.

It does not make databases irrelevant.


The N+1 Query Problem

Consider:

List<Order> orders = repository.findAll();

for (Order order : orders) {
    order.getCustomer().getName();
}

You might expect:

1 query

But depending on the mapping and fetch strategy, you can end up with:

1 query for orders
+
N queries for customers

For 1,000 orders:

1 + 1000
=
1001 queries

This is the classic:

N+1 Query Problem

The ORM is powerful, but understanding what SQL it generates is still essential.


Transactions

Now we reach one of the most important parts of enterprise applications.

Suppose a bank transfer performs:

Debit Account A
Credit Account B

We need both operations to succeed.

We do not want:

Debit A
     |
     v
Application crashes
     |
     v
Credit B never happens

The database should see the operation as one unit.

This is a transaction.

BEGIN
   |
   +-- Debit A
   |
   +-- Credit B
   |
COMMIT

If something fails:

BEGIN
   |
   +-- Debit A
   |
   +-- Credit B
           |
           X
           |
        ROLLBACK

@Transactional

Spring makes declarative transactions simple:

@Transactional
public void transfer(
        Long from,
        Long to,
        BigDecimal amount) {

    debit(from, amount);
    credit(to, amount);
}

Conceptually:

Caller
   |
   v
Spring Proxy
   |
   +-- BEGIN TRANSACTION
   |
   v
transfer()
   |
   +-- debit()
   +-- credit()
   |
   v
Spring Proxy
   |
   +-- COMMIT

If the operation fails according to the configured rollback rules:

BEGIN
   |
   v
Business Method
   |
   X
Exception
   |
   v
ROLLBACK

Spring's declarative transaction support is implemented using AOP proxies and transaction interceptors.


Why @Transactional Is Usually on the Service Layer

Consider:

Controller
    |
    v
Service
    |
    v
Repository

The service layer represents a business operation.

For example:

@Transactional
public void placeOrder(CreateOrderRequest request) {
    createOrder();
    reserveInventory();
    recordPayment();
}

These operations together represent one business transaction.

The controller should normally not need to know the transaction boundaries.

This gives us:

HTTP Layer
     |
     v
Business Operation
     |
     +-- Transaction Boundary
     |
     v
Persistence

A Very Important @Transactional Trap

Consider:

@Service
public class OrderService {

    public void createOrder() {
        saveOrder();
    }

    @Transactional
    public void saveOrder() {
        // database work
    }
}

Calling:

createOrder();

does not necessarily activate the transaction on saveOrder().

Why?

Because Spring's declarative transaction mechanism normally works through a proxy.

Conceptually:

External Call
     |
     v
Spring Proxy
     |
     v
@Transactional Method

But an internal call:

this.saveOrder()

does not go through the Spring proxy.

This is why understanding the mechanism behind annotations is important.


Transaction Rollback

A common assumption is:

Any exception automatically rolls back the transaction.

That is too simplistic.

Spring's default declarative behavior traditionally rolls back for unchecked exceptions such as RuntimeException and Error, while checked exceptions do not automatically trigger rollback under the default rules. Rollback behavior can be customized.

For example:

@Transactional(rollbackFor = PaymentException.class)
public void processPayment() {
}

The important lesson is:

Understand your rollback rules. Do not assume @Transactional means "rollback on everything."


Transactions and External Calls

Consider:

@Transactional
public void createOrder() {

    saveOrder();

    paymentClient.charge();

    sendEmail();
}

This looks convenient.

But now the database transaction may remain open while:

Payment Service
     |
     v
Network
     |
     v
Email Service

responds.

This can hold database connections longer than necessary.

A transaction should generally represent a database consistency boundary, not an arbitrary collection of everything your application does.

This becomes especially important in distributed systems.


REST API Design

Spring makes it easy to create REST APIs.

That does not mean it automatically creates good APIs.

Good API design starts with modeling resources.

For example:

/users
/users/{id}

/orders
/orders/{id}

/products
/products/{id}

rather than:

/getUser
/createUser
/deleteUser
/updateUser

HTTP already provides semantics for common operations.


HTTP Methods

A typical REST API uses:

GET
POST
PUT
PATCH
DELETE

Conceptually:

GET
    Read

POST
    Create

PUT
    Replace

PATCH
    Partially update

DELETE
    Delete

For example:

GET /users/42

means:

Get user 42

while:

DELETE /users/42

means:

Delete user 42

HTTP Status Codes

The response should communicate what happened.

For example:

200 OK
201 Created
204 No Content

400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Unprocessable Content

500 Internal Server Error

Do not return:

200 OK

for every possible situation and put the real status inside JSON.

For example, this is usually a poor design:

{
  "success": false,
  "error": "USER_NOT_FOUND"
}

with:

HTTP/1.1 200 OK

Instead:

HTTP/1.1 404 Not Found

The HTTP protocol already provides semantics.

Use them.


DTOs

Do not automatically expose JPA entities directly through REST APIs.

Instead of:

@GetMapping("/{id}")
public User getUser(...) {
    return repository.findById(...);
}

prefer a response DTO:

public record UserResponse(
        Long id,
        String name,
        String email
) {
}

Then:

@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
    return service.getUser(id);
}

This creates a boundary:

Database Model
      |
      v
Domain Model
      |
      v
API DTO
      |
      v
JSON

Now changes to your database model do not automatically become API changes.


Pagination

Never assume that:

GET /users

should return every user.

Suppose the database contains:

10 million users

Returning all of them is obviously problematic.

Use pagination.

For example:

GET /users?page=0&size=20

The response might contain:

{
  "content": [
    ...
  ],
  "page": 0,
  "size": 20,
  "totalElements": 10000000
}

For very large datasets, cursor-based pagination may be more appropriate than offset pagination.


Idempotency

Another important REST concept is idempotency.

An operation is idempotent when repeating it produces the same intended result.

For example:

PUT /users/42

with the same representation can be repeated.

But:

POST /payments

may create multiple payments if the client retries.

For important operations such as:

Payments
Orders
Financial Transactions

you may need an idempotency key.

For example:

Idempotency-Key: 7f9e...

The server can recognize that the request has already been processed.

This becomes extremely important when networks fail.


Validation

Clients cannot always be trusted to send valid data.

Consider:

{
  "email": "not-an-email",
  "age": -10
}

The application should reject this before business logic executes.

Bean Validation provides declarative constraints.

For example:

public record CreateUserRequest(

    @NotBlank
    String name,

    @Email
    @NotBlank
    String email,

    @Min(18)
    int age
) {
}

Then:

@PostMapping
public UserResponse create(
        @Valid @RequestBody CreateUserRequest request) {

    return service.create(request);
}

The flow becomes:

HTTP Request
     |
     v
JSON Parsing
     |
     v
Validation
     |
     +---- invalid ----> 400
     |
     v
Controller
     |
     v
Service

Spring Boot supports Bean Validation when a validation implementation such as Hibernate Validator is available.


Why Validate at the API Boundary?

Suppose the API accepts:

age = -50

There is no reason to let that value travel through:

Controller
    |
    v
Service
    |
    v
Repository
    |
    v
Database

Validation should reject structurally invalid input as early as possible.

But there is an important distinction.

Validation is not the same as business rules.

For example:

@Min(18)
int age;

is structural validation.

But:

User must be at least 18 to open this particular account type.

may be a business rule.

That belongs in the domain/service logic.


Exception Handling

Applications fail.

A database can be unavailable.

A user can request something that does not exist.

Another service can timeout.

A request can contain invalid data.

The important question is not:

Can my application avoid all exceptions?

It cannot.

The important question is:

How does my application turn failures into predictable behavior?


Bad Exception Handling

A common pattern is:

try {
    ...
} catch (Exception e) {
    return null;
}

This is dangerous.

It hides the actual failure.

Now the caller sees:

null

instead of:

Database unavailable

Another bad pattern is:

catch (Exception e) {
    e.printStackTrace();
}

Production applications need structured error handling.


Centralized Exception Handling

Spring provides:

@RestControllerAdvice

For example:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    ResponseEntity<ApiError> handleUserNotFound(
            UserNotFoundException ex) {

        return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(new ApiError(
                        "USER_NOT_FOUND",
                        ex.getMessage()
                ));
    }
}

Now controllers do not need:

try {
    ...
} catch (...) {
    ...
}

everywhere.

Instead:

Controller
    |
    X
Exception
    |
    v
@RestControllerAdvice
    |
    v
Consistent HTTP Error

Consistent Error Responses

A good API should have a predictable error format.

For example:

{
  "code": "USER_NOT_FOUND",
  "message": "User 42 does not exist",
  "timestamp": "2026-08-30T14:00:00Z",
  "path": "/users/42"
}

Clients can then reliably handle errors.

Instead of:

Sometimes string
Sometimes object
Sometimes null
Sometimes HTML

the API provides a stable contract.


Never Leak Internal Exceptions

Suppose the database throws:

org.postgresql.util.PSQLException

The client does not need to see:

jdbc:postgresql://internal-db:5432/prod

or a stack trace.

External errors should communicate useful information without exposing internal implementation details.

Think in terms of:

Internal Exception
       |
       v
Error Mapping
       |
       v
Public API Error

Problem Details

Modern HTTP APIs can also use the standardized Problem Details format.

Conceptually:

{
  "type": "https://example.com/problems/user-not-found",
  "title": "User Not Found",
  "status": 404,
  "detail": "User 42 does not exist",
  "instance": "/users/42"
}

The important idea is that errors should be part of the API contract.


Observability

An application that works locally is not necessarily an application that can be operated in production.

Imagine a user reports:

The API is slow.

What do you need to know?

Which endpoint?
Which request?
Which instance?
How slow?
When did it happen?
Database slow?
External service slow?
CPU high?
Memory high?
Errors increasing?

This is where observability becomes important.

Observability is commonly described through three pillars:

Logs
Metrics
Traces

Spring Boot integrates with Micrometer Observation for metrics and traces.


Logging

Logs tell us what happened.

For example:

log.info(
    "Order created orderId={} userId={}",
    orderId,
    userId
);

Avoid:

log.info("Something happened");

Good logs provide context.

For example:

orderId
userId
requestId
operation
duration
result

But be careful.

Never log:

Passwords
Access Tokens
Credit Card Numbers
Secrets
Personal Data

unless there is a specific, carefully controlled reason.


Log Levels

Common log levels include:

TRACE
DEBUG
INFO
WARN
ERROR

A useful mental model:

DEBUG
    Detailed developer information

INFO
    Important normal application events

WARN
    Something unexpected but recoverable

ERROR
    An operation failed

Do not make everything:

log.error(...)

If every message is an error, real errors become difficult to find.


Metrics

Logs tell you about individual events.

Metrics tell you about system behavior over time.

Examples:

HTTP request count
HTTP error count
Request duration
CPU usage
Memory usage
Database connection pool usage
JVM heap usage
GC activity

For an API, a very useful metric is:

Request duration

But average latency alone is often misleading.

Suppose:

99 requests = 20 ms
1 request  = 10 seconds

The average may hide the bad request.

This is why percentiles are important.


Percentiles

Consider:

p50 = 20 ms
p95 = 40 ms
p99 = 100 ms

p99 = 100 ms means roughly:

99% of measured requests completed in 100 ms or less.

The remaining 1% took longer.

This is extremely useful for understanding tail latency.

A production API should therefore often monitor:

p50
p95
p99

rather than only:

average

Distributed Tracing

Now imagine:

Client
   |
   v
API Gateway
   |
   v
Order Service
   |
   +----> User Service
   |
   +----> Payment Service
   |
   +----> Inventory Service
                  |
                  v
              Database

The request may cross many processes.

If the request becomes slow, logs alone can make investigation difficult.

Distributed tracing associates operations with a trace.

Conceptually:

Trace ID: abc123

Gateway
   |
   +-- 10ms
       |
       v
Order Service
   |
   +-- 50ms
   |
   +----> User Service
   |          |
   |          +-- 20ms
   |
   +----> Payment Service
              |
              +-- 800ms

Now we can immediately see:

Payment Service
       |
       v
800ms

is probably responsible for most of the latency.


Spring Boot Actuator

Spring Boot Actuator provides production-oriented endpoints and infrastructure for monitoring and managing applications.

Common endpoints include:

/actuator/health
/actuator/info
/actuator/metrics

A health endpoint can answer:

Is the application alive?

and, depending on configuration:

Are important dependencies healthy?

This becomes useful for:

Kubernetes
Load Balancers
Monitoring Systems
Deployment Platforms

Health vs Readiness

There is an important operational distinction.

Liveness

asks:

Is this process alive?

while:

Readiness

asks:

Can this instance receive traffic?

For example:

Application process = alive
Database connection = unavailable

The process might still be alive but should not necessarily receive normal application traffic.

This distinction becomes particularly important in containerized environments.


Putting Everything Together

A production Spring Boot application can look like this:

                    Client
                      |
                      v
                HTTP Request
                      |
                      v
             +----------------+
             | Spring Security|
             +----------------+
                      |
                      v
             +----------------+
             | Spring Web/MVC  |
             +----------------+
                      |
                      v
             +----------------+
             |   Controller   |
             +----------------+
                      |
                      v
             +----------------+
             |    Service     |
             +----------------+
                      |
                 Transaction
                      |
                      v
             +----------------+
             | Spring Data    |
             +----------------+
                      |
                      v
             +----------------+
             | JPA / Hibernate|
             +----------------+
                      |
                      v
                   Database

Around all of this:

              +------------------+
              |  Observability    |
              |                  |
              | Logs             |
              | Metrics          |
              | Traces           |
              +------------------+

And cross-cutting infrastructure:

Configuration
Security
Validation
Exception Handling
Transactions
Observability

A Typical Spring Boot Project

A reasonable project structure might look like:

src/main/java/com/example/orders

    Application.java

    config/
        SecurityConfig.java
        JacksonConfig.java

    controller/
        OrderController.java
        UserController.java

    service/
        OrderService.java
        UserService.java

    repository/
        OrderRepository.java
        UserRepository.java

    entity/
        Order.java
        User.java

    dto/
        CreateOrderRequest.java
        OrderResponse.java
        UserResponse.java

    exception/
        OrderNotFoundException.java
        GlobalExceptionHandler.java

    security/
        JwtAuthenticationFilter.java

The exact package structure is not mandatory.

The important thing is separation of responsibilities.


A Complete Request

Let's follow a request through the application.

Suppose the client sends:

POST /orders
Authorization: Bearer eyJ...
Content-Type: application/json

with:

{
  "productId": 42,
  "quantity": 2
}

The request travels through the system.

Step 1: Security

Spring Security checks:

Token
   |
   v
Authentication
   |
   v
Authorization

If the user is not allowed:

403 Forbidden

Step 2: HTTP Parsing

Spring Web converts:

{
  "productId": 42,
  "quantity": 2
}

into:

CreateOrderRequest

Step 3: Validation

Spring validates:

@NotNull
Long productId;

@Positive
int quantity;

Invalid input stops here.


Step 4: Controller

The controller calls:

orderService.createOrder(request);

Step 5: Transaction

The service starts a transaction:

BEGIN

Step 6: Business Logic

The service:

Find Product
Check Inventory
Create Order
Reserve Inventory

Step 7: Persistence

Spring Data interacts with:

JPA
   |
Hibernate
   |
JDBC
   |
Database

Step 8: Commit

If everything succeeds:

COMMIT

Step 9: Response

The service returns:

OrderResponse

Spring serializes it:

{
  "id": 123,
  "status": "CREATED"
}

Step 10: Observability

Throughout the request, the application can record:

Trace ID
Request duration
Database timing
HTTP status
Error count
Business metrics
Logs

Now the entire request is observable.


The Most Important Spring Boot Mental Model

Spring Boot is not primarily about annotations.

It is about infrastructure.

Consider:

@RestController
@Service
@Repository
@Transactional
@Valid
@PreAuthorize

It is tempting to memorize what each annotation does.

That is useful, but it is not enough.

The deeper model is:

Spring Container
      |
      v
Beans
      |
      v
Dependency Injection
      |
      v
Infrastructure
      |
      +-- Web
      +-- Security
      +-- Transactions
      +-- Data
      +-- Validation
      +-- Observability

Annotations are often metadata that tells Spring how to apply that infrastructure.


Spring Is a Runtime System Around Your Application

Your business code might be:

public OrderResponse createOrder(
        CreateOrderRequest request) {

    // business logic
}

But the actual runtime behavior may be:

HTTP
 |
 v
Security Filter
 |
 v
Spring MVC
 |
 v
Validation
 |
 v
Controller Proxy / Interceptors
 |
 v
Transactional Proxy
 |
 v
Service
 |
 v
Repository Proxy
 |
 v
Hibernate
 |
 v
JDBC
 |
 v
Database

And alongside it:

Logging
Metrics
Tracing

This is why Spring applications can look deceptively simple.

A few lines of application code may activate a very large amount of framework infrastructure.


Common Mistakes

Putting Business Logic in Controllers

Bad:

@PostMapping
public Order create(...) {

    validate();
    calculatePrice();
    checkInventory();
    save();
    sendNotification();

    return ...;
}

Prefer:

@PostMapping
public OrderResponse create(
        @Valid @RequestBody CreateOrderRequest request) {

    return orderService.create(request);
}

Controllers should primarily handle HTTP concerns.


Exposing Entities Directly

Avoid:

return userRepository.findById(id).orElseThrow();

as your public API contract.

Prefer DTOs.


Putting Transactions Everywhere

Do not blindly annotate every method:

@Transactional

Think about the business transaction boundary.


Ignoring Generated SQL

Using JPA does not mean you can ignore SQL.

Always understand:

What SQL is being generated?
How many queries?
Which indexes are used?
Are joins correct?
Is there an N+1 problem?

Catching Every Exception

Avoid:

catch (Exception e) {
    // ignore
}

Handle exceptions deliberately.


Logging Sensitive Information

Never casually log:

password
token
secret
card number

Observability must not become a data-leak mechanism.


Treating Security as an Afterthought

Do not build the entire API and then think:

"Let's add security now."

Security affects:

API design
Authentication
Authorization
Data access
Error handling
Logging
Deployment

Spring Boot Is Not a Replacement for Understanding

Spring Boot can make this code:

@Repository
public interface UserRepository
        extends JpaRepository<User, Long> {
}

look incredibly simple.

But the database is still doing:

SQL
Indexes
Locks
Transactions
Disk I/O
Query Planning

Similarly:

@Transactional

looks simple.

But underneath it are:

Proxy
Interceptor
Transaction Manager
Connection
Database Transaction
Commit
Rollback

And:

@RestController

looks simple.

But underneath it are:

HTTP
Servlet Container
Filters
DispatcherServlet
Handler Mapping
Argument Resolution
Message Conversion
Serialization

The goal is therefore not to memorize Spring annotations.

The goal is to understand what the framework is doing for you.


A Useful Learning Order

If you are learning Spring Boot, a good progression is:

Java
  |
  v
Spring Core
  |
  +-- IoC
  +-- Dependency Injection
  +-- Beans
  +-- Configuration
  |
  v
Spring Boot
  |
  +-- Auto Configuration
  +-- Starters
  +-- Profiles
  +-- Configuration
  |
  v
Spring Web
  |
  +-- HTTP
  +-- Controllers
  +-- Serialization
  +-- Filters
  |
  v
Spring Data
  |
  +-- JPA
  +-- Hibernate
  +-- Queries
  +-- Transactions
  |
  v
Spring Security
  |
  +-- Authentication
  +-- Authorization
  +-- Security Filters
  |
  v
Validation
  |
  v
Exception Handling
  |
  v
Observability

Once these concepts are understood, most Spring Boot applications become much easier to read.


The Bigger Picture

A Spring Boot application is not just:

Controller
    |
    v
Service
    |
    v
Repository

A production application is closer to:

                         Client
                           |
                           v
                    +-------------+
                    | HTTP / TLS  |
                    +-------------+
                           |
                           v
                    +-------------+
                    |  Security   |
                    +-------------+
                           |
                           v
                    +-------------+
                    | Spring Web  |
                    +-------------+
                           |
                           v
                    +-------------+
                    | Validation  |
                    +-------------+
                           |
                           v
                    +-------------+
                    | Controller  |
                    +-------------+
                           |
                           v
                    +-------------+
                    |   Service   |
                    +-------------+
                           |
                      Transaction
                           |
                           v
                    +-------------+
                    | Spring Data |
                    +-------------+
                           |
                           v
                    +-------------+
                    | JPA/Hibernate|
                    +-------------+
                           |
                           v
                       Database

             +-------------------------+
             |      Observability      |
             |                         |
             | Logs | Metrics | Traces |
             +-------------------------+

Spring Boot provides the infrastructure that connects these pieces.

Your job is to make the business behavior correct.


Final Thoughts

Spring Boot is powerful because it removes enormous amounts of infrastructure boilerplate.

But that convenience can also hide what is actually happening.

When you write:

@RestController

understand HTTP and Spring MVC.

When you write:

@Autowired

or use constructor injection, understand dependency injection and the Spring container.

When you write:

@Transactional

understand transactions, proxies, rollback, connection lifetimes, and database consistency.

When you use:

JpaRepository

understand SQL, indexes, joins, fetching, and query performance.

When you configure:

Spring Security

understand authentication and authorization rather than treating security as a collection of configuration snippets.

When you add:

@Valid

understand the boundary between input validation and business rules.

And when you add:

Logs
Metrics
Traces

remember that observability is not decoration.

It is how you understand what your system is doing after it leaves your development machine.

The real value of Spring Boot is therefore not that it lets you write less code.

It is that it provides a coherent infrastructure for building applications:

             Spring Boot
                  |
      +-----------+-----------+
      |           |           |
      v           v           v
     Web       Security      Data
      |           |           |
      +-----------+-----------+
                  |
                  v
            Business Logic
                  |
                  v
             Transactions
                  |
                  v
              Database
                  |
                  v
           Observability

Once you understand these boundaries and how they interact, Spring Boot stops being a collection of annotations and starts becoming what it really is:

A framework for composing the infrastructure required to run a production Java application.