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
@Transactionalmeans "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.