4 Key Tips: Production-Ready Spring Boot REST API with PostgreSQL

Neeraj Mandal

4 Key Tips: Production-Ready Spring Boot REST API with PostgreSQL

In our recent engagement we had an opportunity to create a REST API leveraging Spring Boot and PostgresSQL. The shift from development to production can be quite the leap. Deploying on Azure offers a cloud-compatible environment with impressive tools. But beyond compatibility, there’s optimization. Let’s delve deeper into crucial considerations to make your application not just run but shine in a production environment.

To learn more on how to use Spring Data JPA and setup PostgresSQL on Azure follow this article: Use Spring Data JPA with Azure Database for PostgreSQL.

1. Exception Handling: Beyond Azure’s Capabilities

Azure provides a robust suite of monitoring and diagnostic tools you can use to log exceptions and capture diagnostics, but structuring your application-specific errors within your Spring application can help provide useful messages to end-users.

The @ControllerAdvice annotation in Spring allows you to define a global exception handler, and map them to HTTP responses. This ensures that your API has a consistent error response structures across the board. When using global exception handling, if IllegalArgumentException error is thrown by one of the java libraries in your application, the error will be caught by your global exception handler. Rather than throwing a detailed message exposing the internals of your application, the global exception handler will take the domain specific error, log it and instead, throw a user-friendly error with appropriate HTTP status code.

Example scenario

Consider a situation where a SQL exception occurs. Without global exception handling, the client might receive an obscure error message that exposes the internal details of your application.

Error without global exception handling:

Without global exception handling, following SQL error could occur when creating duplicate username

java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'username123' for key 'users.PRIMARY'

The above error exposes the internal details about our application. But with the @ControllerAdvice annotation, you can catch this SQL exception and transform it into a user-friendly message, while also logging the details for internal review.

Here is how to setup the GlobalExceptionHandler class and use the @ControllerAdvice annotation:

@ControllerAdvice  
@Slf4j 
public class GlobalExceptionHandler {  

    @ExceptionHandler(value = Exception.class)  
    public ResponseEntity<Object> handleGenericException(Exception e) {  

        log.warn("API Request | Exception caught during request processing", e);   

        if (e instanceof IllegalArgumentException) {
            return new ResponseEntity<>("Request is invalid, please verify field name and values.", HttpStatus.BAD_REQUEST);
        } else if (e instanceof SQLIntegrityConstraintViolationException) { 
            log.error("Database constraint violation occurred", e); 
            return new ResponseEntity<>("Username already taken. Please choose a different one.", HttpStatus.BAD_REQUEST); 
         } else {  
            return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);  
        }  
    }  

    // You can add more exception handlers below for specific exceptions. 
 } 

Result

With the global exception handler in place, if the user tries to create duplicate username an error will be logged for internal review and the user will receive a user-friendly error message.

Error: Username already taken. Please choose a different one.

2. Data Validation: Enforcing Integrity

Azure can serve as the backing data store for your application, but the validity of your application’s data rests in your hands. This is where JPA validations come in – for example, for an entity User:

@Entity
public class User {

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

    @NotBlank(message = "Name is mandatory")
    private String name;

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

    // getters, setters, etc.
}

In the above code snippet, if the name field in the User object is left blank then a javax.validation.ConstraintViolationException exception will be thrown with message “Name is mandatory”.

Error Example:

javax.validation.ConstraintViolationException: Validation failed for classes [com.example.User] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
    ConstraintViolationImpl{interpolatedMessage='Name is mandatory', propertyPath=name, rootBeanClass=class com.example.User, messageTemplate='{javax.validation.constraints.NotBlank.message}'}
]

3. API Security: Leverage Azure and Spring Security

Azure Spring Apps and Spring Security are very compatible. Integrate them for a potent combination that guards your application endpoints.

It is a good practice to use Spring’s Security to ensure that users have the appropriate level of access to the application based on their roles (e.g. admin, user, manager, guest, support). Identity providers like Microsoft Entra ID can serve as the store of your users and roles, and you can use Spring Security to ensure that those users are only permitted to access the right methods in your application controllers, services and repositories.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .httpBasic();
    }
}

To learn more about Spring Boot and application security, see Securing a Spring Boot Web Application. To learn how to integrate Microsoft Entra ID with Spring Boot, check out Spring Boot Starter for Azure Active Directory developer’s guide.

4. Optimization: Performance Tweaks

Optimize static queries

While Azure can help scale up your app, you can also implement performance optimizations to make your application itself more efficient. By specifying the query using Spring Data JPA annotations, we can optimize the SQL executed by a Spring repository and improve app’s performance:

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

    @Query("SELECT u FROM User u WHERE u.email = ?1")
    User findByEmailAddress(String email);
}

Connection pooling and caching

Connection pooling and caching can also help in applications under heavy database load. These features can be managed via the Spring app configuration in application.yml or and by leveraging annotations @Cacheable on your entity methods whose return values do not change often. For further reading, see A Guide To Caching in Spring.

It’s common to author Spring entities that have relationships to others. In this pattern, we might see something like a collection of Book entities available as a property on a BookStore entities (i.e., a one-to-many relationship). If you select for a very large number of BookStore entities, you see a large number of queries getting executed causing slow responses.

Pay special attention to the @FetchMode annotation when authoring such relationships. What’s known as the “N+1 problem” can often result in numerous, small selects being executed to populate the related entities on the owning object. Using Subselect fetch mode can greatly improve performance in these circumstances.


In conclusion, while Azure paves the way for a smooth deployment, the nuances in these considerations ensure your application runs optimally, securely, and resiliently. Happy coding and deploying! 🚀

Feedback usabilla icon