Sitemap
Dolle Priyanka

Dolle Priyanka

🌟 Welcome to my corner of creativity! I’m Priyanka, a passionate writer on a mission to share stories that resonate, inspire change, and uplift society. πŸ“šβœοΈ

Spring Boot Cheat Sheet β€” Part 2: From Basic API to Production-Ready πŸš€

6 min readSep 16, 2026

--

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
↓
Database

We 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
↓
Database

And for the response:

Database
↓
Payment Entity
↓
PaymentResponse DTO
↓
API Response

This 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:

@Valid

It 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
↓
Service

Invalid requests can be rejected before they reach the business logic.

3️⃣ Exception Handling

Now imagine we request:

GET /payments/999

But 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:

@RestControllerAdvice

comes 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/999

can return:

HTTP 404 NOT FOUND

instead of exposing an unexpected stack trace.

The flow becomes:

Controller
↓
Service
↓
Exception
↓
GlobalExceptionHandler
↓
HTTP Response

This 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 request
201 CREATED
β†’ Resource successfully created
400 BAD REQUEST
β†’ Invalid request
401 UNAUTHORIZED
β†’ Authentication required
403 FORBIDDEN
β†’ Access denied
404 NOT FOUND
β†’ Resource doesn't exist
409 CONFLICT
β†’ Request conflicts with current state
500 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 model
DTO
β†’ API model

Why 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
internalReference

But the client may only need:

id
amount
currency
status

DTOs 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
↓
localhost
Test
↓
test database
Production
↓
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=password

But we can make the application more flexible using properties.

payment.currency.default=INR

And 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.properties

We can activate a profile:

spring.profiles.active=dev

Then Spring can load environment-specific configuration.

Mental model:

Development
↓
application-dev.properties
Testing
↓
application-test.properties
Production
↓
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 concerns
Service
β†’ Business logic
Repository
β†’ 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
ERROR

A simple mental model:

DEBUG
β†’ Detailed developer information
INFO
β†’ Normal application events
WARN
β†’ Something unexpected but recoverable
ERROR
β†’ 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 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
↓
Database

And 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 entity
JpaRepository
β†’ Repository with built-in CRUD operations
@RestControllerAdvice
β†’ Global REST exception handling
@ExceptionHandler
β†’ Handles specific exceptions
ResponseEntity
β†’ Controls HTTP response/status
application.properties
β†’ Application configuration
@Value
β†’ Inject configuration value
@Profile
β†’ Environment-specific configuration
@Service
β†’ Business logic
@Repository
β†’ Data access
Logger
β†’ Application logging

🎯 The Bigger Picture

At this point, our mental model has evolved.

Part 1:

Request
↓
Controller
↓
Service
↓
Repository
↓
Database

Part 2 adds the pieces needed for a more realistic application:

Client
↓
HTTP Request
↓
Controller
↓
@Valid
↓
DTO
↓
Service
↓
Business Logic
↓
Repository
↓
Entity
↓
Database

And when something goes wrong:

Exception
↓
@RestControllerAdvice
↓
HTTP Error Response

This 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.

--

--

Dolle Priyanka
Dolle Priyanka

🌟 Welcome to my corner of creativity! I’m Priyanka, a passionate writer on a mission to share stories that resonate, inspire change, and uplift society. πŸ“šβœοΈ