Spring Boot Cheat Sheet β Part 2: From Basic API to Production-Ready π
In Part 1, we built the basic mental model of a Spring Boot application.We saw how a request flows through:
Client
β
Controller
β
Service
β
Repository
β
DatabaseWe also covered important concepts like:
@SpringBootApplication@RestController@Service@Repository- Dependency Injection
@RequestBody@PathVariable@RequestParam- Constructor Injection
That gives us the foundation.But thereβs an important question:
What happens when we build a real API?
Real APIs need validation.
They need proper error handling.
They need database persistence.
They shouldnβt expose internal entity objects directly.
And they need configuration that can change between environments.
So in Part 2, letβs take our Payment API one step further. π
1οΈβ£ DTOs β Donβt Expose Your Entity Directly
Suppose we have a Payment entity:
@Entity
public class Payment {@Id
@GeneratedValue
private Long id; private BigDecimal amount; private String currency; private String method; private String status;
}
A beginner might directly use this entity in the controller:
@PostMapping
public Payment createPayment(@RequestBody Payment payment) {
return paymentService.createPayment(payment);
}It works.
But in a real application, we usually donβt want our database entity to become our API contract.
Instead, we introduce DTOs.
DTO = Data Transfer Object
For example:
public class PaymentRequest {private BigDecimal amount; private String currency; private String method;
}
And:
public class PaymentResponse {private Long id; private BigDecimal amount; private String currency; private String method; private String status;
}
Now we have a clear separation:
API Request
β
PaymentRequest DTO
β
Service
β
Payment Entity
β
DatabaseAnd for the response:
Database
β
Payment Entity
β
PaymentResponse DTO
β
API ResponseThis separation becomes very useful as the application grows.
2οΈβ£ Request Validation
What if someone sends this request?
{
"amount": -500,
"currency": "",
"method": null
}We donβt want invalid data reaching our business logic.
Spring Boot supports validation using annotations.
For example:
public class PaymentRequest {@NotNull
@Positive
private BigDecimal amount; @NotBlank
private String currency; @NotBlank
private String method;
}
Then in the controller:
@PostMapping
public PaymentResponse createPayment(
@Valid @RequestBody PaymentRequest request) { return paymentService.createPayment(request);
}The important part is:
@ValidIt tells Spring to validate the incoming request.
For example:
@NotNull
β value must not be null@NotBlank
β String cannot be null, empty, or whitespace@Positive
β number must be greater than zero@Size
β controls minimum/maximum size
This gives us an important flow:
HTTP Request
β
DTO
β
Validation
β
ServiceInvalid requests can be rejected before they reach the business logic.
3οΈβ£ Exception Handling
Now imagine we request:
GET /payments/999But payment 999 doesn't exist.
Should the service simply return null?
Usually, no.
We can create a custom exception:
public class PaymentNotFoundException
extends RuntimeException { public PaymentNotFoundException(Long id) {
super("Payment not found: " + id);
}
}Then our service can throw it:
public PaymentResponse getPayment(Long id) {Payment payment = paymentRepository
.findById(id)
.orElseThrow(() ->
new PaymentNotFoundException(id)); return mapToResponse(payment);
}
But we still need to convert this exception into a proper HTTP response.
Thatβs where:
@RestControllerAdvicecomes in.
4οΈβ£ @RestControllerAdvice
We can create a global exception handler:
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(PaymentNotFoundException.class)
public ResponseEntity<String> handlePaymentNotFound(
PaymentNotFoundException ex) { return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
}
Now:
GET /payments/999can return:
HTTP 404 NOT FOUNDinstead of exposing an unexpected stack trace.
The flow becomes:
Controller
β
Service
β
Exception
β
GlobalExceptionHandler
β
HTTP ResponseThis is much cleaner than putting try-catch blocks everywhere.
5οΈβ£ HTTP Status Codes Matter
A REST API shouldnβt return 200 OK for everything.
Common status codes include:
200 OK
β Successful request201 CREATED
β Resource successfully created400 BAD REQUEST
β Invalid request401 UNAUTHORIZED
β Authentication required403 FORBIDDEN
β Access denied404 NOT FOUND
β Resource doesn't exist409 CONFLICT
β Request conflicts with current state500 INTERNAL SERVER ERROR
β Unexpected server-side error
For example, after creating a payment:
return ResponseEntity
.status(HttpStatus.CREATED)
.body(paymentResponse);Now the client knows that a new resource was created.
6οΈβ£ Spring Data JPA
So far weβve talked about the repository.
Now letβs actually connect it to the database.
Our entity:
@Entity
public class Payment {@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private BigDecimal amount; private String currency; private String method; private String status;
}
And our repository:
public interface PaymentRepository
extends JpaRepository<Payment, Long> {
}Thatβs it.
We donβt need to write implementations for common operations.
Spring Data JPA provides methods such as:
findById()
save()
deleteById()
findAll()
existsById()For example:
paymentRepository.save(payment);or:
Optional<Payment> payment =
paymentRepository.findById(id);7οΈβ£ Custom Repository Queries
Suppose we want all successful payments.
Get Dolle Priyankaβs stories inΒ yourΒ inbox
Join Medium for free to get updates fromΒ thisΒ writer.
We could define:
List<Payment> findByStatus(String status);Spring Data JPA can derive the query from the method name.
So:
findByStatus(...)essentially represents a query based on the status field.
We could call:
paymentRepository.findByStatus("SUCCESS");Similarly:
findByCurrency(String currency)or:
findByAmountGreaterThan(BigDecimal amount)This is one of the useful features of Spring Data JPA.
8οΈβ£ Entity vs DTO
This distinction is important enough to remember.
Entity
Represents the persistence model.
@Entity
public class Payment {
}DTO
Represents data transferred through the API.
public class PaymentRequest {
}Think:
Entity
β Database modelDTO
β API modelWhy separate them?
Because the database structure and API contract donβt always need to be the same.
For example, our database might contain:
id
amount
currency
method
status
createdAt
updatedAt
internalReferenceBut the client may only need:
id
amount
currency
statusDTOs allow us to control what enters and leaves the API.
9οΈβ£ Configuration
Real applications donβt hardcode everything.
For example:
String databaseUrl =
"jdbc:mysql://localhost:3306/paymentdb";This is not ideal.
Different environments may have different databases:
Local
β
localhostTest
β
test databaseProduction
β
production database
Spring Boot provides configuration files for this.
For example:
spring.datasource.url=jdbc:mysql://localhost:3306/paymentdb
spring.datasource.username=root
spring.datasource.password=passwordBut we can make the application more flexible using properties.
payment.currency.default=INRAnd access it from Java:
@Value("${payment.currency.default}")
private String defaultCurrency;Now the value can be changed without changing Java code.
π Profiles
What if we need different configuration for different environments?
Thatβs where Spring Profiles are useful.
For example:
application.properties
application-dev.properties
application-test.properties
application-prod.propertiesWe can activate a profile:
spring.profiles.active=devThen Spring can load environment-specific configuration.
Mental model:
Development
β
application-dev.propertiesTesting
β
application-test.propertiesProduction
β
application-prod.properties
This is especially important in real-world applications.
1οΈβ£1οΈβ£ Service Layer β Where Business Logic Belongs
Letβs say our payment has a business rule:
Payment amount must be greater than zero.
Or:
A payment cannot be processed if its status is already
SUCCESS.
This is business logic.
It belongs in the service layer.
For example:
@Service
public class PaymentService {public PaymentResponse createPayment(
PaymentRequest request) { if (request.getAmount()
.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException(
"Amount must be greater than zero");
} // business logic return paymentResponse;
}
}
The controller shouldnβt become a giant class containing every business rule.
Keep the responsibilities separate.
Controller
β HTTP concernsService
β Business logicRepository
β Database access
1οΈβ£2οΈβ£ Logging
When something goes wrong in production, System.out.println() isn't enough.
Instead, use logging.
For example:
private static final Logger log =
LoggerFactory.getLogger(PaymentService.class);Then:
log.info("Processing payment: {}", paymentId);For errors:
log.error(
"Failed to process payment: {}",
paymentId,
exception
);Common log levels:
TRACE
DEBUG
INFO
WARN
ERRORA simple mental model:
DEBUG
β Detailed developer informationINFO
β Normal application eventsWARN
β Something unexpected but recoverableERROR
β Something failed
Good logging becomes extremely useful when debugging production issues.
1οΈβ£3οΈβ£ The Complete Flow
Now our Payment API looks much more realistic.
Client
β
β HTTP Request
β
βββββββββββββββββββ
β Controller β
ββββββββββ¬βββββββββ
β
β
Request Validation
β
β
βββββββββββββββββββ
β Service β
ββββββββββ¬βββββββββ
β
Business Logic
β
β
βββββββββββββββββββ
β Repository β
ββββββββββ¬βββββββββ
β
β
DatabaseAnd errors can flow through:
Exception
β
@RestControllerAdvice
β
Proper HTTP Responseπ§ Spring Boot Part 2 Cheat Sheet
Save this one. π
DTO
β Data Transfer Object@Valid
β Validate incoming request@NotNull
β Value cannot be null@NotBlank
β String cannot be null/empty/whitespace@Positive
β Number must be greater than zero@Entity
β JPA persistence entityJpaRepository
β Repository with built-in CRUD operations@RestControllerAdvice
β Global REST exception handling@ExceptionHandler
β Handles specific exceptionsResponseEntity
β Controls HTTP response/statusapplication.properties
β Application configuration@Value
β Inject configuration value@Profile
β Environment-specific configuration@Service
β Business logic@Repository
β Data accessLogger
β Application logging
π― The Bigger Picture
At this point, our mental model has evolved.
Part 1:
Request
β
Controller
β
Service
β
Repository
β
DatabasePart 2 adds the pieces needed for a more realistic application:
Client
β
HTTP Request
β
Controller
β
@Valid
β
DTO
β
Service
β
Business Logic
β
Repository
β
Entity
β
DatabaseAnd when something goes wrong:
Exception
β
@RestControllerAdvice
β
HTTP Error ResponseThis is the difference between simply knowing Spring Boot annotations and actually understanding how a Spring Boot backend is structured.
But weβre still not done.
A production application also needs things like:
- Authentication and authorization
- Transactions
- Pagination
- Database relationships
- API documentation
- Testing
- Actuator and monitoring
- Caching
- Security
- Microservices communication
And thatβs where Spring Boot starts getting really interesting. π
In Part 3, weβll take our Payment API into the database properly and understand JPA relationships, transactions, pagination, and the problems that appear when our application starts handling real data.







