Spring Boot - Part 3 (Exception Handling,JPA Pagination, Swagger, Deployment)

 

Exception Handling

Exception Handling is the process of intercepting these exceptions before they reach the client and returning a clean meaningful response with the correct HTTP status code instead. In Spring Boot exception handling is done in 3 parts — 

  • Custom exception classes that describe what went wrong, 
  • An error response class that defines what the client receives, and a 
  • Global exception handler that intercepts every exception in your app and returns the right response automatically.

Step 1 — Custom Exception Classes

Right now you are throwing a generic RuntimeException everywhere :


throw new RuntimeException("User not found"); // ❌ generic, tells nothing

The problem is every error in your app looks the same. You cannot tell a "user not found" error apart from an "email already exists" error. They are both just RuntimeException. The solution is to create specific exception classes for each type of error. Every custom exception just extends RuntimeException and passes a meaningful message :


// exception/UserNotFoundException.java
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(int id) {
super("User with id " + id + " not found");
}
}

// exception/EmailAlreadyExistsException.java
public class EmailAlreadyExistsException extends RuntimeException {
public EmailAlreadyExistsException(String email) {
super("Email " + email + " already exists");
}
}

Now instead of throwing a generic exception you throw a specific one :


throw new UserNotFoundException(99); // ✅ specific and meaningful
throw new EmailAlreadyExistsException("a@b.com"); // ✅ specific and meaningful

NOTE : All custom exceptions extend RuntimeException — not Exception. This keeps them unchecked so you do not need try catch blocks everywhere in your service layer.


Step 2 — Error Response Class

Now that you have specific exceptions you need a clean object to send back to the client. Instead of Spring's ugly default error object you create your own :


// exception/ErrorResponse.java
public class ErrorResponse {

private int status;
private String message;
private String timestamp;

public ErrorResponse(int status, String message) {
this.status = status;
this.message = message;
this.timestamp = java.time.LocalDateTime.now().toString();
}

public int getStatus() { return status; }
public String getMessage() { return message; }
public String getTimestamp() { return timestamp; }

}

When an error occurs the client will receive this clean JSON :


{
"status": 404,
"message": "User with id 99 not found",
"timestamp": "2024-01-15T10:30:00"
}


Step 3 — Global Exception Handler

You have custom exceptions and a clean error response. Now you need something that sits between your app and the client, catches every exception, and returns the right ErrorResponse with the right HTTP status.

This is done using @RestControllerAdvice. It marks a class as a global exception handler — Spring automatically routes all exceptions thrown anywhere in your app to this class. Inside it you define a method for each exception using @ExceptionHandler which tells Spring which specific exception that method handles.



// exception/GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
ErrorResponse error = new ErrorResponse(404, ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}

@ExceptionHandler(EmailAlreadyExistsException.class)
public ResponseEntity<ErrorResponse> handleEmailExists(EmailAlreadyExistsException ex) {
ErrorResponse error = new ErrorResponse(409, ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.CONFLICT);
}

// Always last — catches any unexpected exception
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
ErrorResponse error = new ErrorResponse(500, "Something went wrong");
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}

}


NOTE : The order of @ExceptionHandler methods matters. Always put the most specific exceptions first and Exception.class last. Spring picks the first matching handler it finds. If you put Exception.class first it will catch everything and your specific handlers will never be called.

NOTE : You only need one GlobalExceptionHandler class in your entire project. It automatically handles exceptions from every controller. Never put exception handling logic inside individual controllers. If you have multiple handlers and two of them handle the same exception Spring does not know which one to use. The behavior becomes unpredictable and hard to debug.


Step 4 — Throwing Custom Exceptions in Service

Now update your service to throw custom exceptions. Any exception throws goes through the global exception handlers and intercepted correctly.


@Service
public class UserService {

@Autowired
private UserRepository userRepository;

public User getUserById(int id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}

public User createUser(User user) {
if (userRepository.existsByEmail(user.getEmail())) {
throw new EmailAlreadyExistsException(user.getEmail());
}
return userRepository.save(user);
}

}


Step 5 — ResponseEntity in Controller

ResponseEntity represents the entire HTTP response — the body and the status code. You use it in your controller to return the correct status code with every response. Without it Spring always returns 200 OK even when you create a resource which should be 201 Created :


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

@Autowired
private UserService userService;

@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers()); // 200
}

@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable int id) {
return ResponseEntity.ok(userService.getUserById(id)); // 200
// if not found — GlobalExceptionHandler returns 404 automatically
}

@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return new ResponseEntity<>(userService.createUser(user), HttpStatus.CREATED); // 201
// if email exists — GlobalExceptionHandler returns 409 automatically
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable int id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build(); // 204
}

}



## How Everything Flows Together

GET /users/99
UserController.getUserById(99)
UserService.getUserById(99)
userRepository.findById(99) → empty Optional
throws UserNotFoundException("User with id 99 not found")
@RestControllerAdvice intercepts it
@ExceptionHandler(UserNotFoundException.class) is called
Returns :
{
"status" : 404,
"message" : "User with id 99 not found",
"timestamp" : "2024-01-15T10:30:00"
}



@RestControllerAdvice

When Spring starts up and sees a class marked with @RestControllerAdvice it registers it as the global exception interceptor. From that point on whenever any exception is thrown anywhere in your entire app Spring automatically routes it to this class first.  @RestControllerAdvice tells Spring — "I have a class that knows how to handle exceptions. Send all exceptions there before sending anything to the client."

When an exception is thrown in your app it bubbles up through Service → Controller. When it reaches the Controller Spring does not know what to do with it and just sends back a generic ugly 500 error to the client.


// exception/GlobalExceptionHandler.java

@RestControllerAdvice
public class GlobalExceptionHandler {
// all exception handling logic lives here
}

It is a combination of two annotations :

  • @ControllerAdvice — makes this class apply globally to all controllers in your application. Without this the class would only work for one specific controller.
  • @ResponseBody — makes every method in this class automatically convert its return value to JSON. Without this Spring would try to return a view instead of JSON.

You combine both into @RestControllerAdvice which is cleaner and the standard way in REST APIs.

NOTE : You only ever need one @RestControllerAdvice class in your entire project. It automatically intercepts exceptions from every single controller. Never create two — if two handlers handle the same exception Spring does not know which one to pick and behavior becomes unpredictable.


@ExceptionHandler

Now that Spring knows your GlobalExceptionHandler class handles exceptions, it needs to know which method inside that class handles which exception. That is what @ExceptionHandler does.

You put @ExceptionHandler on a method and tell it which exception class it is responsible for. When that specific exception is thrown Spring finds the method with the matching @ExceptionHandler and calls it automatically :


@RestControllerAdvice
public class GlobalExceptionHandler {

// Spring calls this ONLY when UserNotFoundException is thrown
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
// ex — the actual exception object that was thrown
// ex.getMessage() — the message you set when creating the exception
ErrorResponse error = new ErrorResponse(404, ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}

// Spring calls this ONLY when EmailAlreadyExistsException is thrown
@ExceptionHandler(EmailAlreadyExistsException.class)
public ResponseEntity<ErrorResponse> handleEmailExists(EmailAlreadyExistsException ex) {
ErrorResponse error = new ErrorResponse(409, ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.CONFLICT);
}

// Spring calls this when NO other handler matches
// Acts as a safety net for unexpected exceptions
// ALWAYS put this last
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
ErrorResponse error = new ErrorResponse(500, "Something went wrong");
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}

}

You can also handle multiple exceptions in one method when they return the same response :


// Both return 404 so handle them together
@ExceptionHandler({UserNotFoundException.class, ProductNotFoundException.class})
public ResponseEntity<ErrorResponse> handleNotFound(RuntimeException ex) {
ErrorResponse error = new ErrorResponse(404, ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}

NOTE : The order of `@ExceptionHandler` methods matters. Spring picks the first matching handler it finds going top to bottom. Always put specific exceptions first and `Exception.class` last. If you put `Exception.class` first it catches every single exception and your specific handlers below it never get called.


ResponseEntity

By default when you return an object from a controller Spring always sends 200 OK as the status code. But not every response should be 200. Without ResponseEntity you have no way to control which status code gets sent back. Spring just always sends 200. It is a wrapper that holds 2 things :


ResponseEntity
├── Body → the actual data you want to return (User, ErrorResponse etc.)
└── Status → the HTTP status code you want to send (200, 201, 404 etc.)

Think of it like putting your response in an envelope. The envelope is ResponseEntity — it contains the letter (your body) and a label (the status code). Without ResponseEntity Spring just sends the letter with a default 200 label always. How to create a ResponseEntity :


// Full syntax
new ResponseEntity<>(body, HttpStatus.STATUS_CODE)

// Examples
new ResponseEntity<>(user, HttpStatus.OK) // 200 with user
new ResponseEntity<>(user, HttpStatus.CREATED) // 201 with user
new ResponseEntity<>(error, HttpStatus.NOT_FOUND) // 404 with error
new ResponseEntity<>(null, HttpStatus.NO_CONTENT) // 204 with no body


HttpStatus is an enum that contains every standard HTTP status code as a readable named constant. Instead of remembering numbers you use names. You use it with ResponseEntity to set the right status code :


HttpStatus.OK // 200 — request successful
HttpStatus.CREATED // 201 — resource created successfully
HttpStatus.NO_CONTENT // 204 — success but nothing to return
HttpStatus.BAD_REQUEST // 400 — client sent invalid data
HttpStatus.UNAUTHORIZED // 401 — client is not logged in
HttpStatus.FORBIDDEN // 403 — logged in but no permission
HttpStatus.NOT_FOUND // 404 — resource does not exist
HttpStatus.CONFLICT // 409 — resource already exists
HttpStatus.INTERNAL_SERVER_ERROR // 500 — unexpected server error


NOTE : ResponseEntity is used in 2 places — in your controllers to return the correct status on success, and in your GlobalExceptionHandler to return the correct status on error. Together they ensure every single response from your API always has the right HTTP status code.


// ❌ Without ResponseEntity
@PostMapping
public User createUser(@RequestBody User user) {
return userService.createUser(user);
// always returns 200 — wrong, should be 201
}

// ✅ With ResponseEntity
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return new ResponseEntity<>(userService.createUser(user), HttpStatus.CREATED);
// correctly returns 201
}
```

---

### So there are 2 separate reasons to use ResponseEntity

**In Controller** — to return the correct success status code (`200`, `201`, `204` etc.)

**In GlobalExceptionHandler**
      to return the correct error status code (`404`, `409`, `500` etc.)

---

Think of it this way :
```
Controller + ResponseEntity → controls SUCCESS responses
GlobalExceptionHandler + ResponseEntity → controls ERROR responses

Together they ensure every single response from your API has the correct HTTP status code. Without ResponseEntity in the controller your success responses are always wrong even if your error responses are correct.

-----------------------------------------------------------------------------------------------------------------------------

Entity vs Model or DTO

Entity - An Entity is a class marked with @Entity that maps directly to a database table. It represents how your data is stored in the database. Every field in the Entity becomes a column in the table.


@Entity
@Table(name = "users")
public class User {

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

private String name;
private String email;
private String password; // stored in database — hashed
private String role; // stored in database
private boolean active; // stored in database

@CreationTimestamp
private LocalDateTime createdAt;

}

The Problem with Using Entity Everywhere

Imagine you use the User entity directly as your request and response object. Two problems immediately appear :

Problem 1 — Receiving data from client

When a client creates a user they should only send name, email, and password. But if you use the User entity directly the client could also send id, role, active, createdAt — fields they should never be allowed to set.


// Client sends this — they set their own role!
{
"name" : "Deepesh",
"email" : "deepesh@gmail.com",
"password": "secret123",
"role" : "ADMIN" ← security risk
}

Problem 2 — Sending data to client

When you return a user to the client you should never send the password field. But if you return the User entity directly the password goes out to the client.


// Client receives this — password exposed!
{
"id" : 1,
"name" : "Deepesh",
"email" : "deepesh@gmail.com",
"password": "hashed_password_here" ← security risk
}


DTO (Data Transfer Object)

A DTO (Data Transfer Object) is a plain Java class with no @Entity annotation. It is not mapped to any database table. It only exists to carry data between the client and your app — nothing more.

In Spring Boot you will hear both terms — DTO (Data Transfer Object) and Model — used to describe the same thing. They are just different names for a plain Java class with no @Entity annotation that carries data between the client and your application.

You create separate DTOs for what comes in from the client and what goes out to the client :

    // DTO for receiving data FROM client — only fields client is allowed to send
public class CreateUserRequest {

@NotBlank(message = "Name cannot be empty")
private String name;

@Email(message = "Must be a valid email")
private String email;

@Size(min = 8, message = "Password must be at least 8 characters")
private String password;

// no id — client never sets id
// no role — client never sets their own role
// no active — client never sets this
// no createdAt — client never sets this

// getters and setters

}

// DTO for sending data TO client — only fields client is allowed to see
public class UserResponse {

private int id;
private String name;
private String email;
private String role;
private LocalDateTime createdAt;

// no password — never expose password to client
// no active — internal field, client does not need this

// getters and setters

}

We can then use it like this in our service and controllers wherever we need to mention the data structure of incoming or outgoing data.


@Service
public class UserService {

@Autowired
private UserRepository userRepository;

public UserResponse createUser(CreateUserRequest request) {

// Convert DTO to Entity
User user = new User();
user.setName(request.getName());
user.setEmail(request.getEmail());
user.setPassword(request.getPassword()); // hash this in real app
user.setRole("USER"); // always set role in service, never from client
user.setActive(true);

// Save Entity to database
User savedUser = userRepository.save(user);

// Convert Entity to Response DTO
UserResponse response = new UserResponse();
response.setId(savedUser.getId());
response.setName(savedUser.getName());
response.setEmail(savedUser.getEmail());
response.setRole(savedUser.getRole());
response.setCreatedAt(savedUser.getCreatedAt());
// password is NOT included in response

return response;

}

}



Entity → maps to database table, used by Repository and JPA
DTO (Request) → what client sends in, has validation annotations
DTO (Response) → what client receives, only safe fields included

Client → DTO (Request) → Entity → Database
Database → Entity → DTO (Response) → Client

NOTE : In small projects and tutorials people often skip DTOs and use the Entity directly to keep things simple. This works but is bad practice because it exposes your database structure and creates security risks. In real production projects you always use DTOs. The extra code is worth the security and flexibility it provides.

-----------------------------------------------------------------------------------------------------------------------------

Validation

Validation is the process of checking incoming data before it reaches your service or database and rejecting it immediately with a meaningful error if it does not meet your rules. Without validation your app happily accepts this garbage data and saves it to the database — corrupting your data and potentially breaking your app.

Spring Boot has a built in validation library. You add annotations to your DTO fields to define the rules and Spring automatically validates incoming data before your controller method even runs. If validation fails Spring throws an exception which your GlobalExceptionHandler catches and returns a clean error response.

Step 1 — Add Dependency

First add the validation library to your pom.xml :


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>


Step 2 — Add Validation Annotations to your DTO

Spring provide different validation annotations, these annotations go on your DTO fields to define the rules. Every annotation has a message property that defines what the client receives when that rule is violated.

You add validation annotations directly on your DTO fields to define the rules. Each annotation has a message property that defines the error message the client receives when that rule is violated :


import jakarta.validation.constraints.*;

public class CreateUserRequest {

@NotBlank(message = "Name cannot be empty")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;

@NotBlank(message = "Email cannot be empty")
@Email(message = "Must be a valid email address")
private String email;

@Min(value = 18, message = "Age must be at least 18")
@Max(value = 100, message = "Age must be less than 100")
private int age;

@NotBlank(message = "Password cannot be empty")
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;

// getters and setters

}

NOTE : Always use @NotBlank for String fields — it is the strictest and covers all 3 cases. Only use @NotNull for non-String types like Integer, Boolean, LocalDate etc.

NOTE : You can stack multiple annotations on one field. Spring runs all of them and collects all failures at once.


Step 3 — @Valid - Enable Validation in Controller

Add @Valid on the @RequestBody parameter in your controller. This tells Spring to validate the incoming object before passing it to your method. Without @Valid Spring ignores all your validation annotations completely :


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

@Autowired
private UserService userService;

@PostMapping
public ResponseEntity<UserResponse>
                   createUser(@Valid @RequestBody CreateUserRequest request) {
// @Valid triggers validation before this method runs
// if validation fails Spring throws
            // MethodArgumentNotValidException automatically
// you never see invalid data inside this method
return new ResponseEntity<>(userService.createUser(request), HttpStatus.CREATED);
}

}

The @Valid is added to every controller that needs to validate its DTO class object validation to check again the data sent by the user. Without it Spring ignores all your validation annotations and never validates anything regardless of what annotations you put on your DTO fields. POST and PUT requests send a body so they always need @Valid.


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

// ✅ Has @RequestBody — add @Valid
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) { }

// ✅ Has @RequestBody — add @Valid
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(@PathVariable int id, @Valid @RequestBody UpdateUserRequest request) { }

// ❌ No @RequestBody — no need for @Valid
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@PathVariable int id) { }

// ❌ No @RequestBody — no need for @Valid
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable int id) { }

}


Step 4 — Handle Errors in GlobalExceptionHandler

When validation fails Spring automatically throws MethodArgumentNotValidException. You handle it in your GlobalExceptionHandler to return a clean error response.

You have 2 options — return only the first error or return all errors at once :


@RestControllerAdvice
public class GlobalExceptionHandler {

// Option 1 — return only the first error
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult()
.getFieldErrors()
.get(0)
.getDefaultMessage();
ErrorResponse error = new ErrorResponse(400, message);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}

// Option 2 — return ALL errors at once (recommended)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult()
.getFieldErrors()
.forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}

// ... other handlers

}

NOTE : Validation annotations go on your DTO — not your @Entity class. The Entity is for database mapping, the DTO is for incoming data validation. Never mix the two.

NOTE : @Valid must be added on the @RequestBody parameter in your controller. Without it Spring ignores all your validation annotations and never validates anything.


## How it all flows

POST /users with invalid data
@Valid triggers validation on @RequestBody
Validation fails — name is empty, email is invalid
Spring throws MethodArgumentNotValidException automatically
GlobalExceptionHandler catches it
Returns 400 Bad Request with all errors :
{
"name" : "Name cannot be empty",
"email" : "Must be a valid email address"
}

 ----------------------------------------------------------------------------------------

1] Receives the incoming JSON
2] Converts it to your DTO object
3] Runs all validation annotations on the fields
4] If everything passes → calls your controller method normally
If anything fails → throws MethodArgumentNotValidException automatically
your controller method is NEVER called
GlobalExceptionHandler catches it and returns 400


You never manually throw MethodArgumentNotValidException. You never write a single line of validation logic. Spring does all of it automatically just because you added @Valid on @RequestBody and validation annotations on your DTO fields.

-----------------------------------------------------------------------------------------------------------------------------

Pagination & Sorting

Instead of fetching everything at once you fetch only what the client needs at that moment, Pagination returns data in small chunks called pagesSorting lets the client decide the order of results. Without it data comes back in whatever order the database feels like — completely unpredictable and unusable.

Spring Data JPA has pagination and sorting completely built in. You do not write a single line of SQL. Just a few small changes to your Repository, Service, and Controller and it works automatically. 

Spring Data JPA generates LIMIT and OFFSET SQL automatically behind the scenes. You never write these yourself. The formula Spring uses :


OFFSET = pageNumber × pageSize

Page 0 → SELECT * FROM products LIMIT 10 OFFSET 0 ← products 1 to 10
Page 1 → SELECT * FROM products LIMIT 10 OFFSET 10 ← products 11 to 20
Page 2 → SELECT * FROM products LIMIT 10 OFFSET 20 ← products 21 to 30

NOTEImplementing pagination and sorting only requires 3 small changes — one in the Repository, one in the Service, and one in the Controller.

JpaRepository already includes PagingAndSortingRepository which gives you pagination methods automatically. You do not need to change anything in the repository layer. 

In service layer Instead of returning List<Product> you return Page<Product>. You accept page number, page size, sort field, and sort direction as parameters and create a Pageable object from them. Spring uses this Pageable to generate the right LIMIT and OFFSET SQL automatically :


// REPOSITORY (No changes needed here)

@Repository
public interface ProductRepository extends JpaRepository<Product, Integer> {
// pagination is already built in — nothing extra needed
}

--------------------------------------------------------------------------------------------

// SERVICE (Accepts pagination parameters and passes them to the repository)

@Service
public class ProductService {

@Autowired
private ProductRepository productRepository;

public Page<Product> getAllProducts(int page, int size, String sortBy, String direction) {

// Build sort direction
Sort sort = direction.equals("desc") ? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();

// Create Pageable — Spring uses this to generate LIMIT and OFFSET
Pageable pageable = PageRequest.of(page, size, sort);

// Pass pageable to repository — Spring handles the rest
return productRepository.findAll(pageable);

}

}


--------------------------------------------------------------------------------------------

// CONTROLLER (Accepts pagination parameters from the request)

@RestController
@RequestMapping("/products")
public class ProductController {

@Autowired
private ProductService productService;

@GetMapping
public ResponseEntity<Page<Product>> getAllProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String direction
) {
return ResponseEntity.ok(productService.getAllProducts(page, size, sortBy, direction));
}

}

NOTE : When Spring returns a Page object it contains much more than just the data. The frontend uses totalPages and totalElements to build pagination UI like "Page 1 of 100" or "Showing 1-10 of 1000 results" :


{
"content" : [ {...}, {...} ], ← the actual data
"pageNumber" : 0, ← current page
"pageSize" : 10, ← items per page
"totalElements" : 1000, ← total items in database
"totalPages" : 100, ← total number of pages
"first" : true, ← is this the first page
"last" : false ← is this the last page
}

Do All JPA Methods Accept Pageable?

No. Only read methods that return multiple results support Pageable. It makes no sense to paginate a save, delete, or single item fetch. When you extend JpaRepository you are not just getting one interface — you are getting a chain of interfaces stacked on top of each other. Each interface in the chain adds more functionality :


JpaRepository ← what you extend
↑ extends
PagingAndSortingRepository ← adds pagination and sorting support
↑ extends
CrudRepository ← adds basic CRUD methods
↑ extends
Repository ← base marker interface



// ✅ Makes sense with Pageable — returns multiple results
findAll(Pageable pageable)
findByCategory(String category, Pageable pageable)

// ❌ Makes no sense with Pageable — single result or no result
save(entity) // saving one record
deleteById(id) // deleting one record
findById(id) // fetching one record
existsById(id) // checking existence
count() // counting records

For your own derived methods and @Query methods you can add Pageable as the last parameter and Spring automatically applies it to the generated query :


// Derived method — just add Pageable as last parameter
Page<Product> findByCategory(String category, Pageable pageable);
// Spring generates : SELECT * FROM products WHERE category = ? LIMIT ? OFFSET ?

// @Query method — just add Pageable as last parameter
@Query("SELECT p FROM Product p WHERE p.price < :price")
Page<Product> findAffordable(@Param("price") double price, Pageable pageable);
// Spring applies LIMIT and OFFSET to your custom query automatically

NOTE : Spring automatically detects the Pageable parameter and applies pagination to the query. You never manually add LIMIT or OFFSET — Spring handles it entirely behind the scenes.


Pagination with Derived Methods

Without pagination your derived method returns everything that matches :


// Without pagination — returns ALL products in that category
List<Product> findByCategory(String category);
// SQL : SELECT * FROM products WHERE category = ?
// if 50,000 products in "Electronics" — returns all 50,000

With pagination you just change the return type to Page<T> and add Pageable as the last parameter. Spring sees the Pageable parameter and automatically adds LIMIT and OFFSET to the generated SQL :


// With pagination — returns only the requested page
Page<Product> findByCategory(String category, Pageable pageable);
// SQL : SELECT * FROM products WHERE category = ? LIMIT ? OFFSET ?
// now returns only 10 products at a time


@Query Methods with Pagination

Same concept applies to @Query methods. You write your JPQL or raw SQL as normal and just add Pageable as the last parameter. Spring takes your query and wraps it with LIMIT and OFFSET automatically :


// Without pagination
@Query("SELECT p FROM Product p WHERE p.price < :price")
List<Product> findAffordable(@Param("price") double price);
// SQL : SELECT * FROM products WHERE price < ?

// With pagination — just add Pageable as last parameter
@Query("SELECT p FROM Product p WHERE p.price < :price")
Page<Product> findAffordable(@Param("price") double price, Pageable pageable);
// SQL : SELECT * FROM products WHERE price < ? LIMIT ? OFFSET ?


NOTE : The Pageable parameter must always be the last parameter in your method. If you put it anywhere else Spring cannot detect it and pagination will not work.

NOTE : You never need to modify your JPQL or SQL query to add pagination. Spring automatically wraps your query with LIMIT and OFFSET based on the Pageable object you pass in. Your query stays clean and focused on filtering — Spring handles the pagination part.


Sorting

Sorting lets you control the order in which data is returned. Spring Data JPA supports sorting both with and without pagination. Sorting controls the order in which data is returned from the database. Spring Data JPA supports sorting across all 3 types of repository methods — built-in methods, derived methods, and @Query methods. Spring automatically adds ORDER BY to the SQL based on how you specify the sort.

Sorting with Built-in Methods

These are the findAll() methods that come from JpaRepository. You pass a Sort object directly :


@Service
public class ProductService {

@Autowired
private ProductRepository productRepository;

public void sortingExamples() {

// Sort by single field ascending
List<Product> byPriceAsc = productRepository.findAll(Sort.by("price").ascending());
// SQL : SELECT * FROM products ORDER BY price ASC

// Sort by single field descending
List<Product> byPriceDesc = productRepository.findAll(Sort.by("price").descending());
// SQL : SELECT * FROM products ORDER BY price DESC

// Sort by multiple fields
List<Product> byPriceThenName = productRepository.findAll(
Sort.by("price").ascending().and(Sort.by("name").descending())
);
// SQL : SELECT * FROM products ORDER BY price ASC, name DESC

// Sort with pagination together
Pageable pageable = PageRequest.of(0, 10, Sort.by("price").ascending());
Page<Product> pagedAndSorted = productRepository.findAll(pageable);
// SQL : SELECT * FROM products ORDER BY price ASC LIMIT 10 OFFSET 0

}

}


Sorting with Derived Methods

You have 2 ways to add sorting to derived methods :

1] Sort in the method name — you define the sort directly in the method name using OrderBy. Spring reads the method name and generates ORDER BY in the SQL automatically. The sort is always fixed — the client cannot change it.


@Repository
public interface ProductRepository extends JpaRepository<Product, Integer> {

// Sort by price ascending
List<Product> findAllByOrderByPriceAsc();
// SQL : SELECT * FROM products ORDER BY price ASC

// Sort by price descending
List<Product> findAllByOrderByPriceDesc();
// SQL : SELECT * FROM products ORDER BY price DESC

// Filter and sort
List<Product> findByCategoryOrderByPriceAsc(String category);
// SQL : SELECT * FROM products WHERE category = ? ORDER BY price ASC

List<Product> findByCategoryOrderByPriceDesc(String category);
// SQL : SELECT * FROM products WHERE category = ? ORDER BY price DESC

// Sort by multiple fields in method name
List<Product> findAllByOrderByPriceAscNameDesc();
// SQL : SELECT * FROM products ORDER BY price ASC, name DESC

}


2] Sort via Pageable — you pass a Pageable object with sort information. The client controls the sort field and direction at runtime. More flexible than method name sorting.



@Repository
public interface ProductRepository extends JpaRepository<Product, Integer> {

// Derived method with Pageable — sort and pagination together
Page<Product> findByCategory(String category, Pageable pageable);
// SQL : SELECT * FROM products WHERE category = ? ORDER BY ? LIMIT ? OFFSET ?

// If you only want sort without pagination pass Sort directly
List<Product> findByCategory(String category, Sort sort);
// SQL : SELECT * FROM products WHERE category = ? ORDER BY ?

}

//---------------------------------------------------------------------------

@Service
public class ProductService {

@Autowired
private ProductRepository productRepository;

// Fixed sort — defined in method name
public List<Product> getProductsByCategoryFixed(String category) {
return productRepository.findByCategoryOrderByPriceAsc(category);
}

// Dynamic sort — client controls sort field and direction
public List<Product> getProductsByCategoryDynamic(String category, String sortBy, String direction) {
Sort sort = direction.equals("desc") ? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
return productRepository.findByCategory(category, sort);
}

// Dynamic sort with pagination
public Page<Product> getProductsByCategoryPaged(String category, int page, int size, String sortBy, String direction) {
Sort sort = direction.equals("desc") ? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
return productRepository.findByCategory(category, pageable);
}

}



Sorting with @Query Methods

You have 2 ways to add sorting to @Query methods :

1] Sort inside the query — you hardcode ORDER BY directly in your JPQL or SQL. The sort is always fixed.


@Repository
public interface ProductRepository extends JpaRepository<Product, Integer> {

// Sort hardcoded in JPQL
@Query("SELECT p FROM Product p WHERE p.price < :price ORDER BY p.price ASC")
List<Product> findAffordableSortedByPrice(@Param("price") double price);
// SQL : SELECT * FROM products WHERE price < ? ORDER BY price ASC

// Sort hardcoded in raw SQL
@Query(value = "SELECT * FROM products WHERE category = :category ORDER BY price DESC",
nativeQuery = true)
List<Product> findByCategorySortedByPriceDesc(@Param("category") String category);
// SQL : SELECT * FROM products WHERE category = ? ORDER BY price DESC

}


2] Sort via Pageable — you add Pageable or Sort as the last parameter. Spring applies the sort to your query dynamically at runtime.


@Repository
public interface ProductRepository extends JpaRepository<Product, Integer> {

// Dynamic sort with Sort parameter — no pagination
@Query("SELECT p FROM Product p WHERE p.price < :price")
List<Product> findAffordable(@Param("price") double price, Sort sort);
// SQL : SELECT * FROM products WHERE price < ? ORDER BY ?

// Dynamic sort with pagination
@Query("SELECT p FROM Product p WHERE p.price < :price")
Page<Product> findAffordablePaged(@Param("price") double price, Pageable pageable);
// SQL : SELECT * FROM products WHERE price < ? ORDER BY ? LIMIT ? OFFSET ?

}



@Service
public class ProductService {

@Autowired
private ProductRepository productRepository;

// Fixed sort — hardcoded in query
public List<Product> getAffordableFixed(double maxPrice) {
return productRepository.findAffordableSortedByPrice(maxPrice);
}

// Dynamic sort — client controls sort
public List<Product> getAffordableDynamic(double maxPrice, String sortBy, String direction) {
Sort sort = direction.equals("desc") ? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
return productRepository.findAffordable(maxPrice, sort);
}

// Dynamic sort with pagination
public Page<Product> getAffordablePaged(double maxPrice, int page, int size, String sortBy, String direction) {
Sort sort = direction.equals("desc") ? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
return productRepository.findAffordablePaged(maxPrice, pageable);
}

}



## Summary — All 3 Methods

Built-in findAll()
├── Sort only → findAll(Sort.by("field").ascending())
└── Sort + Page → findAll(PageRequest.of(page, size, sort))

Derived Methods
├── Method name → findByCategoryOrderByPriceAsc(category)
├── Sort param → findByCategory(category, Sort.by("price").ascending())
└── Pageable param → findByCategory(category, PageRequest.of(page, size, sort))

@Query Methods
├── Hardcoded sort → ORDER BY in JPQL or SQL
├── Sort param → add Sort as last parameter
└── Pageable param → add Pageable as last parameter



-----------------------------------------------------------------------------------------------------------------------------

Swagger

When you build a REST API your frontend developers, mobile developers, or any other team that uses your API need to know :

  • What endpoints exist
  • What data each endpoint expects
  • What each endpoint returns
  • What HTTP method to use
  • What status codes to expect

Without Swagger you would have to write all of this manually in a Word document or Notion page and keep it updated every time your API changes. This is tedious, error prone, and always ends up outdated.

Swagger solves this by automatically generating interactive API documentation directly from your code. It reads your controllers, annotations, and models and generates a beautiful documentation page that is always up to date because it is generated from your actual code.

Setting Up Swagger


<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>

That is literally all you need. Just adding this dependency automatically generates the documentation. No extra configuration required. Go to http://localhost:8080/swagger-ui.html.

Customizing Swagger — Optional Annotations

By default Swagger generates basic documentation from your code. You can add annotations to make it more descriptive and useful :


Swagger Annotations — Simple Example

Imagine you have a simple UserController with 2 endpoints. Without annotations Swagger still generates documentation but it looks generic. With annotations it becomes descriptive and useful :


import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.responses.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.*;

// Groups both endpoints under "Users" in Swagger UI
@Tag(name = "Users", description = "Everything related to users")
@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
private UserService userService;

// Describes what this endpoint does
@Operation(
summary = "Get user by id",
description = "Fetches a single user from the database using their id"
)
// Describes possible responses
@ApiResponses({
@ApiResponse(responseCode = "200", description = "User found successfully"),
@ApiResponse(responseCode = "404", description = "User with this id does not exist")
})
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(
// Describes the path variable
@Parameter(description = "Id of the user you want to fetch", example = "1")
@PathVariable int id) {
return ResponseEntity.ok(userService.getUserById(id));
}

@Operation(
summary = "Create a new user",
description = "Creates a new user and saves them to the database"
)
@ApiResponses({
@ApiResponse(responseCode = "201", description = "User created successfully"),
@ApiResponse(responseCode = "409", description = "Email already exists")
})
@PostMapping
public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
return new ResponseEntity<>(userService.createUser(request), HttpStatus.CREATED);
}

}

// Describes the request body fields
public class CreateUserRequest {

@Schema(description = "Full name of the user", example = "Deepesh Mhatre")
private String name;

@Schema(description = "Email address", example = "deepesh@gmail.com")
private String email;

@Schema(description = "Age of the user", example = "20")
private int age;

}

-----------------------------------------------------------------------------------------------------------------------------

Spring Boot Deployment

The most common way to deploy Spring Boot applications is as a standalone JAR file with an embedded server.


┌─────────────────────────────────────────────────────────────────┐
│ MOST COMMON DEPLOYMENT FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
1. Build: ./mvnw clean package
│ ↓ │
2. Deploy: Copy JAR to server │
│ ↓ │
3. Run: java -jar myapp.jar │
│ ↓ │
4. Access: http://server:8080 │
│ │
└─────────────────────────────────────────────────────────────────┘

JAR (Self-Contained)


┌─────────────────────────────────────────────────────────────────┐
│ myapp.jar │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Your Code │ │ Spring Boot │ │ Embedded │ │
│ │ │ │ Libraries │ │ Tomcat │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
java -jar myapp.jar
┌─────────────────┐
│ App Running │
│ on port 8080
└─────────────────┘


Step 1: pom.xml

xml
<packaging>jar</packaging>  <!-- This is default, can be omitted -->

Step 2: Build the JAR

bash
./mvnw clean package

This creates: target/myapp-0.0.1-SNAPSHOT.jar


Step 3: Run the JAR

bash
java -jar target/myapp-0.0.1-SNAPSHOT.jar

That's it! Your app is now running on port 8080.


Common Variations

Run with custom port

bash
java -jar myapp.jar --server.port=8081

Run with production profile

bash
java -jar myapp.jar --spring.profiles.active=prod


Why JAR is Preferred for Microservices

1. Self-Contained

bash
# One JAR contains everything
myapp.jar
├── Your application code
├── Spring Boot framework
├── Embedded Tomcat server
└── All dependencies

# Run anywhere with just Java
java -jar myapp.jar

2. Consistent Across Environments

text
Developer:   java -jar myapp.jar
Test:        java -jar myapp.jar
Staging:     java -jar myapp.jar
Production:  java -jar myapp.jar
Docker:      java -jar myapp.jar
K8s:         java -jar myapp.jar

SAME COMMAND EVERYWHERE!

3. Docker Friendly

dockerfile
# Simple Dockerfile
FROM openjdk:17-jdk-slim
COPY myapp.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

4. Easy to Scale

bash
# Run multiple instances
java -jar myapp.jar --server.port=8081
java -jar myapp.jar --server.port=8082
java -jar myapp.jar --server.port=8083

5. Cloud-Native

  • Perfect for Kubernetes pods

  • Easy to containerize

  • Stateless (if designed properly)

  • Health checks via Actuator

-----------------------------------------------------------------------------------------------------------------------------



Comments

Popular posts from this blog

React Js + React-Redux (part-2)

React Js + CSS Styling + React Router (part-1)

ViteJS (Module Bundlers, Build Tools)