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.
Fetch mode on related entities
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! 🚀