0% found this document useful (0 votes)
6 views

best-java-microservices-coding-practices

The document is a guide titled 'Best Java Coding Practices to Boost Microservices Productivity,' aimed at helping Java developers optimize their microservices. It covers essential best practices across five critical areas: API design, null handling, logging, exception handling, and database management. By following these practices, developers can improve productivity, code quality, and system resilience in microservices development.

Uploaded by

variety jones
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views

best-java-microservices-coding-practices

The document is a guide titled 'Best Java Coding Practices to Boost Microservices Productivity,' aimed at helping Java developers optimize their microservices. It covers essential best practices across five critical areas: API design, null handling, logging, exception handling, and database management. By following these practices, developers can improve productivity, code quality, and system resilience in microservices development.

Uploaded by

variety jones
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 62

Best Java Coding Practices to

Boost Microservices
Productivity
mezocde
This book is for sale at
http://leanpub.com/best-java-microservices-coding-practices

This version was published on 2024-07-05

This is a Leanpub book. Leanpub empowers authors and


publishers with the Lean Publishing process. Lean Publishing is
the act of publishing an in-progress ebook using lightweight tools
and many iterations to get reader feedback, pivot until you have
the right book and build traction once you do.

© 2024 mezocde
Contents

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

Chapter 2 - Best Practices for API Design . . . . . . . . . . 3


2.1 - Adherence to RESTful Principles . . . . . . . . . . . 3
2.2 - Utilization of Meaningful HTTP Status Codes . . . . 5
2.3 - Strategic API Versioning . . . . . . . . . . . . . . . . 6
2.4 - Graceful Exception Handling . . . . . . . . . . . . . 7
2.5 - Ensuring API Security . . . . . . . . . . . . . . . . . 8
2.6 - Comprehensive API Documentation . . . . . . . . . 9
2.7 - Effective Use of Query Parameters . . . . . . . . . . 10
2.8 - Leveraging HTTP Caching . . . . . . . . . . . . . . . 11
2.9 - Maintaining Intuitive API Design . . . . . . . . . . . 12
2.10 - Enable Response Compression . . . . . . . . . . . . 13
2.11 - Embrace Asynchronous Operations . . . . . . . . . 14
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

Best Practices for Handling Null . . . . . . . . . . . . . . . 18


Understanding Null in Java . . . . . . . . . . . . . . . . . 18
Strategies for Null Handling . . . . . . . . . . . . . . . . . 18

Effective Logging with SLF4J and Logback . . . . . . . . . 23


Choosing SLF4J and Logback . . . . . . . . . . . . . . . . 24
1. Use SLF4J as the Logging Facade . . . . . . . . . . . . . 24
2. Configure Logback for Efficient Logging . . . . . . . . 25
3. Use Appropriate Log Levels . . . . . . . . . . . . . . . . 26
4. Log Meaningful Messages . . . . . . . . . . . . . . . . . 27
5. Use Placeholders for Dynamic Content . . . . . . . . . 28
6. Log Exceptions with Stack Traces . . . . . . . . . . . . 28
7. Use Asynchronous Logging for Performance . . . . . . 29
8. Log at the Appropriate Granularity . . . . . . . . . . . 30
9. Monitor and Rotate Log Files . . . . . . . . . . . . . . . 32
10. Secure Sensitive Information . . . . . . . . . . . . . . 32
11. Structured Logging . . . . . . . . . . . . . . . . . . . . 33
12. Integration with Monitoring Tools . . . . . . . . . . . 34
13. Log Aggregation . . . . . . . . . . . . . . . . . . . . . 34
14. Smart Logging . . . . . . . . . . . . . . . . . . . . . . . 35

Best Practices for Handling Exceptions . . . . . . . . . . . 36


Understanding the Exception Hierarchy . . . . . . . . . . 36
Java Exception Practices: . . . . . . . . . . . . . . . . . . . 37
1. Catch Specific Exceptions . . . . . . . . . . . . . . . . . 37
2. Avoid Swallowing Exceptions . . . . . . . . . . . . . . 38
3. Utilize Finally Blocks or Try-With-Resources . . . . . . 39
4. Document Exceptions . . . . . . . . . . . . . . . . . . . 41
5. Avoid Using Exceptions for Flow Control . . . . . . . . 42
6. Throw Specific and Meaningful Exceptions . . . . . . . 43
7. Prefer Checked Exceptions for Recoverable Conditions 44
8. Wrap Exceptions When Appropriate . . . . . . . . . . . 44
9. Log Exceptions with Relevant Details . . . . . . . . . . 45
10. Handle Exceptions at the Appropriate Layer . . . . . 46

Chapter 6 - Best Practices for Handling Database . . . . . 49


1. Using Repository Abstraction . . . . . . . . . . . . . . . 49
3. Handling Lazy Initialization . . . . . . . . . . . . . . . 52
4. Using Pagination . . . . . . . . . . . . . . . . . . . . . . 54
5. Handling Null Values with Optional . . . . . . . . . . . 55
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
Appendix . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Introduction
Welcome to Best Java Coding Practices to Boost Microservices
Productivity a concise and practical guide designed to help Java
developers optimize their microservices.
Microservices have become increasingly popular in recent years
due to their ability to break down complex systems into smaller,
more manageable components. However, developing microser-
vices in Java requires a deep understanding of best practices to
ensure maximum productivity and efficiency.
This mini-book covers five critical areas that every Java developer
should master to create robust and efficient microservices.

Best Practices for API Design


APIs are the backbone of microservices communication. This
section will guide you through the principles of designing clean,
intuitive, and well-documented APIs that facilitate seamless inte-
gration between services.

Best Practices for Handling Null


Null values can be a major source of bugs and performance issues
in Java applications. Learn how to effectively handle null values
and avoid common pitfalls that can lead to unexpected behavior.

Best Practices for Writing Logs


Logging is crucial for monitoring and debugging microservices.
Discover best practices for writing informative, structured logs that
Introduction 2

provide valuable insights into your system’s behavior.

Best Practices for Handling Exceptions

Exceptions can disrupt the flow of your microservices and impact


overall system stability. Master the art of exception handling to
gracefully manage errors and maintain a smooth user experience.

Best Practices for Database Handling

Efficient database management is vital for microservices perfor-


mance. Learn how to optimize your database interactions, leverage
connection pooling, and ensure data consistency across services.
By following the best practices outlined in this mini book, Java
developers can significantly boost their microservices productivity,
write cleaner code, and build more resilient systems. Whether
you’re a seasoned developer or just starting your microservices
journey, this resource will provide you with the knowledge and
tools necessary to take your skills to the next level.
Chapter 2 - Best Practices
for API Design
Understanding The best practices of API Design in Java is crucial
for Java developers aiming to create robust, scalable Microservices.
In this section, we dive into 11 essential best practices for API
development in Java, which will guide you toward professional and
efficient API creation. By exploring Java-specific examples, you’ll
learn to distinguish between effective and inefficient approaches to
API development.

2.1 - Adherence to RESTful Principles


RESTful architecture is characterized by statelessness, cacheability,
a uniform interface, and a client-server architecture. When APIs
follow these principles, they ensure predictable and standardized
interactions.
Good Example
A GET request to retrieve a resource by ID.
Chapter 2 - Best Practices for API Design 4

1 @RestController
2 @RequestMapping("/users")
3 public class UserController {
4
5 @GetMapping("/{id}")
6 public ResponseEntity<User> getUserById(@PathVariable\
7 Long id) {
8 User user = userService.findById(id);
9 if (user == null) {
10 return ResponseEntity.notFound().build();
11 }
12 return ResponseEntity.ok(user);
13 }
14 }

Avoid Example
Performing an update action using a GET request goes against the
principle that GET requests should be safe and idempotent.

1 @RestController
2 @RequestMapping("/users")
3 public class UserController {
4
5 @GetMapping("/forceUpdateEmail/{id}")
6 public ResponseEntity<Void> forceUpdateUserEmail(
7 @PathVariable Long id,
8 @RequestParam String email) {
9 // This is bad, GET should not change state.
10 userService.updateEmail(id, email);
11 return ResponseEntity.ok().build();
12 }
13 }

Avoid Example
Chapter 2 - Best Practices for API Design 5

Performing an update action using a GET request goes against the


principle that GET requests should be safe and idempotent.

1 @RestController
2 @RequestMapping("/users")
3 public class UserController {
4
5 @GetMapping("/forceUpdateEmail/{id}")
6 public ResponseEntity<Void> forceUpdateUserEmail(@Pat\
7 hVariable Long id,
8 @Req\
9 uestParam String email) {
10 userService.updateEmail(id, email); // This is ba\
11 d, GET should not change state.
12 return ResponseEntity.ok().build();
13 }
14 }

2.2 - Utilization of Meaningful HTTP


Status Codes
HTTP status codes are a critical component of client-server com-
munication, providing immediate insights into the outcome of an
HTTP request.
Good Example
Using 201 Created for successful resource creation.
Chapter 2 - Best Practices for API Design 6

1 @PostMapping("/users")
2 public ResponseEntity<User> createUser(@RequestBody U\
3 ser user) {
4 User savedUser = userService.save(user);
5 return new ResponseEntity<>(savedUser, HttpStatus\
6 .CREATED);
7 }

Avoid Example
Returning 200 OK for a request that fails validation, where a client
error code (4xx) would be more appropriate.

1 @PostMapping("/users")
2 public ResponseEntity<User> createUser(@RequestBody U\
3 ser user) {
4 if (!isValidUser(user)) {
5 return new ResponseEntity<>(HttpStatus.OK); /\
6 / This is bad, should be 4xx error.
7 }
8 User savedUser = userService.save(user);
9 return new ResponseEntity<>(savedUser, HttpStatus\
10 .CREATED);
11 }

2.3 - Strategic API Versioning


Versioning APIs is essential to manage changes over time without
disrupting existing clients. This practice allows developers to
introduce changes or deprecate API versions systematically.
Good Example Explicitly versioning the API in the URI.
Chapter 2 - Best Practices for API Design 7

1 @RestController
2 @RequestMapping("/api/v1/users")
3 public class UserController {
4 // RESTful API actions for version 1
5 }

Avoid Example Lack of versioning, leading to potential breaking


changes for clients.

1 @RestController
2 @RequestMapping("/users")
3 public class UserController {
4 // API actions with no versioning.
5 }

2.4 - Graceful Exception Handling


Robust error handling enhances API usability by providing clear,
actionable information when something goes wrong.
Good Example A specific exception handler for a resource not
found scenario, returning a 404 Not Found.

1 @ExceptionHandler(UserNotFoundException.class)
2 public ResponseEntity<Object> handleUserNotFound(User\
3 NotFoundException ex) {
4 return new ResponseEntity<>(ex.getMessage(), Http\
5 Status.NOT_FOUND);
6 }

Avoid Example Exposing stack traces to the client, can be a security


risk and unhelpful to the client.
Chapter 2 - Best Practices for API Design 8

1 @ExceptionHandler(Exception.class)
2 public ResponseEntity<Object> handleAllExceptions(Exc\
3 eption ex) {
4 return new ResponseEntity<>(ex.getStackTrace(), H\
5 ttpStatus.INTERNAL_SERVER_ERROR); // This is bad, don't e
6 xpose stack trace.
7 }

2.5 - Ensuring API Security


Security is paramount, and APIs should implement appropriate au-
thentication, authorization, and data validation to protect sensitive
data.
Good Example
Incorporating authentication checks within the API endpoint.

1 @GetMapping("/users/{id}")
2 public ResponseEntity<User> getUserById(@PathVariable Lon\
3 g id) {
4 if (!authService.isAuthenticated(id)) {
5 return new ResponseEntity<>(HttpStatus.UNAUTHORIZ\
6 ED);
7 }
8 // Fetch and return the user
9 }

Avoid Example
Omitting security checks, leaving the API vulnerable to unautho-
rized access.
Chapter 2 - Best Practices for API Design 9

1 @GetMapping("/users/{id}")
2 public ResponseEntity<User> getUserById(@PathVariable Lon\
3 g id) {
4 // No authentication check, this is bad.
5 // Fetch and return the user
6 }

2.6 - Comprehensive API


Documentation
Well-documented APIs facilitate ease of use and integration, reduc-
ing the learning curve for developers and encouraging adoption.
Good Example
Using annotation-based documentation tools like Swagger.

1 @Api(tags = "User Management")


2 @RestController
3 @RequestMapping("/api/v1/users")
4 public classUserController {
5 // RESTful API actions with Swagger annotations for d\
6 ocumentation
7 }

Avoid Example
Non-existent documentation makes it difficult to discover the us-
ability of APIs.
Chapter 2 - Best Practices for API Design 10

1 @RestController
2 @RequestMapping("/users")
3 public class UserController {
4 // API actions with no comments or documentation anno\
5 tations
6 }

2.7 - Effective Use of Query


Parameters
Query parameters should be employed for filtering, sorting, and
pagination to enhance the API’s flexibility and prevent the transfer
of excessive data.
Good Example
API endpoints that accept query parameters for sorting and pagi-
nated responses.

1 @GetMapping("/users")
2 public ResponseEntity<List<User>> getUsers(
3 @RequestParam Optional<String> sortBy,
4 @RequestParam Optional<Integer> page,
5 @RequestParam Optional<Integer> size) {
6 // Logic for sorting and pagination
7 return ResponseEntity.ok(userService.getSortedAndPagi\
8 natedUsers(
9 sortBy, page, size));
10 }

Avoid Example
Endpoints that return all records without filtering or pagination,
potentially overwhelming the client.
Chapter 2 - Best Practices for API Design 11

1 @GetMapping("/users")
2 public ResponseEntity<List<User>> getAllUsers() {
3 // This is bad, as it might return too much data
4 return ResponseEntity.ok(userService.findAll());
5 }

2.8 - Leveraging HTTP Caching


Caching can significantly improve performance by reducing server
load and latency. It’s an optimization that experts should not
overlook.
Good Example
Implementing ETags and making use of conditional requests.

1 @GetMapping("/users/{id}")
2 public ResponseEntity<User> getUserById(@PathVariable Lon\
3 g id,
4 @RequestHeader(va\
5 lue = "If-None-Match", required = false) String ifNoneMat
6 ch) {
7 User user = userService.findById(id);
8 String etag = user.getVersionHash();
9
10 if (etag.equals(ifNoneMatch)) {
11 return ResponseEntity.status(HttpStatus.NOT_MODIF\
12 IED).build();
13 }
14
15 return ResponseEntity.ok().eTag(etag).body(user);
16 }

Avoid Example
Chapter 2 - Best Practices for API Design 12

Ignoring caching mechanisms leads to unnecessary data transfer


and processing.

1 @GetMapping("/users/{id}")
2 public ResponseEntity<User> getUserById(@PathVariable Lon\
3 g id) {
4 // No ETag or Last-Modified header used, this is bad \
5 for performance
6 return ResponseEntity.ok(userService.findById(id));
7 }

2.9 - Maintaining Intuitive API Design


An API should be self-explanatory, with logical resource naming,
predictable endpoint behavior, and consistent design patterns.
Good Example
Clear and concise endpoints that immediately convey their func-
tionality.

1 @PostMapping("/users")
2 public ResponseEntity<User> createUser(@RequestBody User \
3 user) {
4 // Endpoint clearly indicates creation of a user
5 }
6
7 @GetMapping("/users/{id}")
8 public ResponseEntity<User> getUserById(@PathVariable Lon\
9 g id) {
10 // The action of retrieving a user by ID is clear
11 }

Avoid Example
Chapter 2 - Best Practices for API Design 13

Confusing or convoluted endpoint paths and actions that obfuscate


their purpose.

1 @PutMapping("/user-update")
2 public ResponseEntity<User> updateUser(@RequestBody User \
3 user) {
4 // This is bad, as the path does not indicate a resou\
5 rce
6 }

2.10 - Enable Response Compression


To optimize network performance, enabling response compression
is a smart move. It reduces the payload size, which can significantly
decrease network latency and speed up client-server interactions.
Good Example
Configuring your web server or application to use gzip or Brotli
compression for API responses.

1 // In Spring Boot, you might configure application.proper\


2 ties to enable response compression.
3
4 server.compression.enabled=true
5 server.compression.mime-types=application/json,applicatio\
6 n/xml,text/html,text/xml,text/plain

This configuration snippet tells the server to compress responses


for specified MIME types.
Avoid Example
Sending large payloads without compression leads to increased
load times and bandwidth usage.
Chapter 2 - Best Practices for API Design 14

1 // No configuration or code in place to handle response c\


2 ompression.
3
4 @GetMapping("/users")
5 public ResponseEntity<List<User>> getAllUsers() {
6 // This could return a large JSON payload that isn't \
7 compressed, which is inefficient.
8 return ResponseEntity.ok(userService.findAll());
9 }

2.11 - Embrace Asynchronous


Operations
Asynchronous operations are essential for handling long-running
tasks, such as processing large datasets or batch operations. They
free up client resources and prevent timeouts for operations that
take longer than the usual HTTP request-response cycle.
Good Example Using asynchronous endpoints that return a 202
Accepted status code with a location header to poll for results.

1 @PostMapping("/users/batch")
2 public ResponseEntity<Void> batchCreateUsers(@RequestBody\
3 List<User> users) {
4 CompletableFuture<Void> batchOperation = userService.\
5 createUsersAsync(users);
6 HttpHeaders responseHeaders = new HttpHeaders();
7 responseHeaders.setLocation(URI.create("/users/batch/\
8 status"));
9
10 return ResponseEntity.accepted().headers(responseHead\
11 ers).build();
12 }
Chapter 2 - Best Practices for API Design 15

This example accepts a batch creation request and processes it asyn-


chronously, providing a URI for the client to check the operation’s
status.
Avoid Example Blocking operations for batch processing that keep
the client waiting indefinitely.

1 @PostMapping("/users/batch")
2 public ResponseEntity<List<User>> batchCreateUsers(@Reque\
3 stBody List<User> users) {
4 // This is a synchronous operation that may take a lo\
5 ng time to complete.
6 List<User> createdUsers = userService.createUsers(use\
7 rs);
8 return ResponseEntity.ok(createdUsers);
9 }

This example performs a synchronous batch creation, which could


lead to a timeout or a poor user experience due to the long wait
time.
By incorporating response compression and embracing
asynchronous operations, API developers can greatly improve
performance and user experience. These practices are essential
when dealing with modern web applications and services that
require efficient real-time data processing and transmission.

Summary
The following guidelines help ensure your Java APIs are robust, se-
cure, and user-friendly, enhancing both performance and developer
experience.
Chapter 2 - Best Practices for API Design 16

Best Practices for Effective API Design in


Java

Follow RESTful Principles

• Utilize statelessness, cacheability, and uniform interfaces.


• Do: Use GET for retrieving data.
• Don’t: Use GET for actions that change state.

Use Meaningful HTTP Status Codes

• Choose status codes that accurately reflect the API response.


• Do: 404 Not Found for non-existent resources.
• Don’t: Misuse 200 OK for errors.

Implement API Versioning

• Manage changes without breaking existing functionality.


• Do: Version your APIs in the URI.
• Don’t: Deploy APIs without version control.

Handle Exceptions Gracefully

• Provide clear error messages and actionable feedback.


• Do: Customize error handling based on exception type.
• Don’t: Expose stack traces to end-users.

Secure Your API

• Ensure authentication, authorization, and data validation are


in place.
• Do: Protect endpoints using security checks.
• Don’t: Leave APIs open to unauthorized access.
Chapter 2 - Best Practices for API Design 17

Document APIs Thoroughly

• Make integration and usage straightforward with clear doc-


umentation.
• Do: Use tools like Swagger.
• Don’t: Leave APIs undocumented.

Utilize Query Parameters Effectively

• Support filtering, sorting, and pagination through query pa-


rameters.
• Do: Allow extensive query options.
• Don’t: Overwhelm clients with unfiltered data.

Enable HTTP Caching

• Improve performance with caching strategies like ETags.


• Do: Implement caching to reduce load.
• Don’t: Overload clients and networks with unnecessary data
re-fetching.

Ensure Intuitive Design

• Maintain logical, predictable API endpoints.


• Do: Design endpoints that clearly indicate their function.
• Don’t: Confuse users with misleading paths.

Support Asynchronous Operations

• Facilitate handling of long-running tasks without blocking


clients.
• Do: Provide endpoints for status checks on asynchronous
requests.
• Don’t: Force clients to wait for prolonged operations.
Best Practices for
Handling Null
As a Java engineer, dealing with null references can be quite
challenging. A mistake can cause the entire application to crash
due to the dreaded NullPointerException, especially while dealing
with third parties and Microservices architecture. In this section we
will provide a roadmap for null-safe programming in Java, enabling
you to write more stable and bug-free code.

Understanding Null in Java


Before we delve into handling nulls, let’s understand the impli-
cations. Null references were introduced in Java to represent the
absence of a value. However, Tony Hoare, the inventor of the
null reference, has called it his “billion-dollar mistake” due to the
number of errors it can cause in programming.

Strategies for Null Handling


Let’s explore the strategies that can help Java Engineers write null-
safe code, ensuring applications are more reliable and maintainable.

1. Explicit Null Checks: The First Line of


Defense
When to use: Use explicit null checks when dealing with code that
interacts with external libraries or systems where you have little
Best Practices for Handling Null 19

control over the input.


The simplest way to guard against null is an explicit check:

1 if (user != null) {
2 user.updateProfile();
3 }

2. Embracing Optional: A Modern Approach

When to use: Optional is best used as a return type where there


might not be a value to return, and when you want to avoid null
checks.
Java 8 introduced the Optional class, which can help you express a
variable that might be null more explicitly:

1 Optional<OptionalUser> optionalUser = userRepository.find\


2 ById(userId);
3 optionalUser.ifPresent(User::updateProfile);

3. Assertions: Catching Bugs Early

When to use: Assertions are ideal in the development and testing


phases but should not be relied upon for public API argument
checking.
Assertions are a way to document assumptions and catch bugs
during development:

1 assert user != null;


Best Practices for Handling Null 20

4. Annotations: Self-Documenting Code


When to use: Annotations are a great way to communicate
nullability contracts and help static analysis tools identify potential
null-related errors.
Annotations like @NonNull and @Nullable can be used to indicate
when a method parameter, return type, or field can or cannot be
null:

1 public void updateUserProfile(@NonNull User user) {}

5. Null Object Pattern: Avoiding Null Checks


When to use: This pattern is useful when you want to avoid
multiple null checks and when it makes sense to have default
behavior.
The Null Object Pattern involves creating an object with default
behavior or no behavior:

1 public class NullUser extends User {


2 @Override
3 public void updateProfile(ProfileInfo info) {}
4 }
5
6 User user = userRepository.findById(userId).orElse(new Nu\
7 llUser());
8 user.updateProfile(profileInfo);

6. Libraries: Third-Party Helpers


When to use: Turn to libraries when you want robust, tried-and-
tested methods to handle null values and are open to adding third-
party dependencies.
Best Practices for Handling Null 21

Libraries like Apache Commons Lang provide utility methods for


handling null. For example:

1 String value = StringUtils.defaultIfEmpty(getStringMayBeN\


2 ull(), "defaultString");

7. Design by Contract: Enforcing Usage


When to use: Design by contract is essential for public APIs, as it
enforces the proper use of methods and helps prevent errors caused
by improper arguments.
Define clear contracts for your methods:

1 public void updateUserProfile(User user) {


2 Objects.requireNonNull(user, "User cannot be null");
3 }

8. Safe Navigation Operator: A Future


Prospect
While Java does not currently have the Elvis operator (?.), it’s worth
looking out for in future versions. It’s popular in languages like
Groovy for handling nulls gracefully.

9. Java 8+ Stream API: Functional Null


Handling
When to use: The Stream API is best when working with collec-
tions or streams of data where you want to filter out null values in
a clean, functional style.
Java’s Stream API provides a functional approach to handle possi-
ble null values in collections:
Best Practices for Handling Null 22

1 public void updateMultipleUsers(List<String> userIds) {


2 userIds.stream()
3 .filter(Objects::nonNull)
4 .map(userRepository::findById)
5 .filter(Optional::isPresent)
6 .map(Optional::get)
7 .forEach(this::updateUserProfile);
8 }

10. Failing Fast: Early Detection

When to use: Failing fast is a general best practice that applies


across many programming scenarios, not just null handling.
Throw exceptions as soon as a null value is detected:

1 public void updateUserProfile(User user) {


2 if (user == null) {
3 throw new IllegalArgumentException("User cannot be nu\
4 ll");
5 }
6 }
Effective Logging with
SLF4J and Logback
Effective Logging is an essential aspect of any Java application, pro-
viding insights into its operational state. It is especially crucial in
production environments, where it aids in debugging, monitoring,
and incident response. In this comprehensive guide, we will explore
the effective practices for using SLF4J with Logback, ensuring a
reliable and maintainable logging strategy.
By following these best practices, developers and operations teams
can leverage SLF4J and Logback to turn logs into strategic resources
for application management and incident resolution. Embrac-
ing these guidelines will lead to improved observability, quicker
troubleshooting, and a deeper understanding of system behavior,
establishing a solid foundation for application reliability and per-
formance.

Key Benefits of Effective Logging

• Improved observability: Logs provide a detailed record of


application behavior, making it easier to understand how the
system is operating and identify potential issues.
• Faster troubleshooting: Well-structured and informative logs
enable developers to quickly pinpoint the root cause of prob-
lems and resolve them efficiently.
• Enhanced incident response: Logs are invaluable during in-
cident response, providing a chronological account of events
leading up to and during an issue.
• Compliance and security: Logs can serve as evidence of com-
pliance with regulations and help identify security breaches
Effective Logging with SLF4J and Logback 24

or suspicious activities.

Choosing SLF4J and Logback


SLF4J (Simple Logging Facade for Java) is a popular logging facade
that provides a consistent API for logging across different logging
frameworks. Logback is a widely used logging framework that of-
fers a rich set of features and customization options. By combining
SLF4J with Logback, you can benefit from the flexibility and power
of both tools.
In this guide, we will cover 14 essential best practices for using
SLF4J and Logback effectively in your Java applications. These
practices will help you achieve reliable, maintainable, and infor-
mative logging that supports your application’s operational needs.

1. Use SLF4J as the Logging Facade


Good Practice:
Choose SLF4J as your application’s logging facade to decouple
your logging architecture from the underlying logging library
implementation. This abstraction allows you to switch between
different logging frameworks without major code changes.
Effective Logging with SLF4J and Logback 25

1 import org.slf4j.Logger;
2 import org.slf4j.LoggerFactory;
3
4 public class MyClass {
5 private static final Logger logger = LoggerFactory.getL\
6 ogger(MyClass.class);
7 // ...
8 }

Avoid Practice:
Hardcoding a specific logging framework implementation in your
application code can lead to difficulties when needing to switch
libraries.

1 import org.apache.log4j.Logger;
2
3 public class MyClass {
4 private static final Logger logger = Logger.getLogger(M\
5 yClass.class);
6 // ...
7 }

2. Configure Logback for Efficient


Logging
Good Practice:
Externalize your Logback configuration and use ‘PatternLayout‘
for improved performance and flexibility. Define different config-
urations for development, staging, and production environments to
better manage the verbosity and detail of logs.
Effective Logging with SLF4J and Logback 26

1 <configuration>
2
3 <appender name="STDOUT" class="ch.qos.logback.core.Cons\
4 oleAppender">
5 <encoder>
6 <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logge\
7 r{36} - %msg%n</pattern>
8 </encoder>
9 </appender>
10
11 <root level="debug">
12 <appender-ref ref="STDOUT" />
13 </root>
14
15 </configuration>

Avoid Practice:
Using an outdated or non-performant layout class and hardcoding
configuration settings in the code can make it difficult to adapt to
different environments.

1 <configuration>
2
3 <appender name="STDOUT" class="ch.qos.logback.core.Cons\
4 oleAppender">
5 <layout class="ch.qos.logback.classic.PatternLayout">
6 <!-- Non-recommended layout configuration -->
7 </layout>
8 </appender>
9
10 <!-- ... -->
11
12 </configuration>
Effective Logging with SLF4J and Logback 27

3. Use Appropriate Log Levels


Good Practice:
Log at the correct level to convey the importance and intention of
the message. Use ‘INFO‘ for general events, ‘DEBUG‘ for detailed
information during development, and ‘ERROR‘ for serious issues
that need attention.

1 logger.info("Application has started.");


2 logger.debug("The value of X is {}", x);
3 logger.error("Unable to process the request.", e);

Avoid Practice:
Logging everything at the same level, can overwhelm the log files
with noise and make it difficult to spot critical issues.

1 logger.error("Application has started."); // Incorrect us\


2 e of log level
3 logger.error("The value of X is " + x); // Inefficient st\
4 ring concatenation
5 // ...

4. Log Meaningful Messages


Good Practice:
Include relevant information such as transaction or correlation IDs
in your log messages to provide context. This is especially helpful
in distributed systems for tracing requests across services.
Effective Logging with SLF4J and Logback 28

1 logger.info("Order {} has been processed successfully.", \


2 orderId);

Avoid Practice:
Vague or generic log messages that do not provide sufficient context
to understand the event or issue.

1 logger.info("Processed successfully."); // No context pro\


2 vided

5. Use Placeholders for Dynamic


Content
Good Practice:
Utilize placeholders to avoid unnecessary string concatenation
when the log level is disabled, saving memory and CPU cycles.

1 logger.debug("User {} logged in at {}", username, LocalDa\


2 teTime.now());

Avoid Practice:
Concatenating strings within log statements is less efficient.

1 logger.debug("User " + username + " logged in at " + Loca\


2 lDateTime.now());

6. Log Exceptions with Stack Traces


Good Practice:
Always log the full exception, including the stack trace, to provide
maximum context for diagnosing issues.
Effective Logging with SLF4J and Logback 29

1 try {
2 // some code that throws an exception
3 } catch (Exception e) {
4 logger.error("An unexpected error occurred", e);
5 }

Avoid Practice:
Logging only the exception message without the stack trace can
omit critical diagnostic information.

1 try {
2 // some code that throws an exception
3 } catch (Exception e) {
4 logger.error("An unexpected error occurred: " + e.getMe\
5 ssage());
6 }

7. Use Asynchronous Logging for


Performance
Good Practice:
Implement asynchronous logging to improve application perfor-
mance by offloading logging activities to a separate thread.
Effective Logging with SLF4J and Logback 30

1 <configuration>
2
3 <appender name="ASYNC" class="ch.qos.logback.classic.As\
4 yncAppender">
5 <appender-ref ref="FILE" />
6 </appender>
7
8 <appender name="FILE" class="ch.qos.logback.core.FileAp\
9 pender">
10 <file>application.log</file>
11 <encoder>
12 <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logge\
13 r{36} - %msg%n</pattern>
14 </encoder>
15 </appender>
16
17 <root level="INFO">
18 <appender-ref ref="ASYNC" />
19 </root>
20
21 </configuration>

Avoid Practice:
Synchronous logging in performance-critical paths without consid-
ering the potential for log-related latency.

1 logger.info("A time-sensitive operation has completed.");

8. Log at the Appropriate


Granularity
Good Practice:
Effective Logging with SLF4J and Logback 31

You should balance between logging too much and too little. Log
at the appropriate granularity based on the specific requirements
of your application. Avoid excessive logging that clutters the logs
and makes it difficult to identify important information.

1 public void processOrder(Order order) {


2
3 logger.info("Processing order: {}", order.getId());
4
5 // Logging at a finer granularity for debugging purposes
6 logger.debug("Order details: {}", order);
7
8 // Process the order
9 orderService.save(order);
10
11 logger.info("Order processed successfully");
12 }

Avoid Practice:
Excessive logging at a high granularity in production, can lead to
performance issues and log flooding.

1 public void processOrder(Order order) {


2
3 logger.trace("Entering processOrder method");
4 logger.debug("Received order: {}", order);
5 logger.info("Processing order: {}", order.getId());
6
7 // Logging every step of order processing
8 logger.debug("Step 1: Validating order");
9 // ...
10 logger.debug("Step 2: Calculating total amount");
11 // ...
12 logger.debug("Step 3: Updating inventory");
13 // ...
Effective Logging with SLF4J and Logback 32

14
15 logger.info("Order processed successfully");
16 logger.trace("Exiting processOrder method");
17 }

9. Monitor and Rotate Log Files


Good Practice:
Configure log file rotation based on size or time to prevent logs
from consuming excessive disk space. Set up monitoring for log
files to trigger alerts when nearing capacity.
Avoid Practice:
Letting log files grow indefinitely, can lead to disk space exhaustion
and potential system failure.

10. Secure Sensitive Information


Good Practice:
Implement filters or custom converters in your logging framework
to redact or hash sensitive data before it’s written to the logs.

1 log.info("Processing payment with card: {}", maskCreditCa\


2 rd(creditCardNumber));
3
4 public String maskCreditCard(String creditCardNumber) {
5 int length = creditCardNumber.length();
6 if (length < 4) return "Invalid number";
7 return "****-****-****-" + creditCardNumber.substring(l\
8 ength - 4);
9 }
Effective Logging with SLF4J and Logback 33

Avoid Practice:
Logging sensitive information such as passwords, API keys, Credit
Cards, or personally identifiable information (PII).

1 log.info("Processing payment with card: {}", creditCardNu\


2 mber);

11. Structured Logging


Good Practice:
Adopt structured logging to output logs in a machine-readable
format like JSON, facilitating better searching and indexing in log
management systems.

1 <configuration>
2
3 <appender name="JSON_CONSOLE" class="ch.qos.logback.cor\
4 e.ConsoleAppender">
5 <encoder class="net.logstash.logback.encoder.LoggingE\
6 ventCompositeJsonEncoder">
7 <providers>
8 <timestamp>
9 <timeZone>UTC</timeZone>
10 </timestamp>
11 <version />
12 <logLevel />
13 <threadName />
14 <loggerName />
15 <message />
16 <context />
17 <stackTrace />
18 </providers>
Effective Logging with SLF4J and Logback 34

19 </encoder>
20 </appender>
21
22 <root level="info">
23 <appender-ref ref="JSON_CONSOLE" />
24 </root>
25
26 </configuration>

Let’s take a look at an example log message that is printed in JSON


format:

1 logger.info("Order has been processed");

The output of the above log message will be printed as below:

1 {"@timestamp":"2024-03-26T15:52:00.789Z","@version":"1","\
2 message":"Order has been processed","logger_name":"Applic
3 ation","thread_name":"main","level":"INFO"}

Avoid Practice:
Using unstructured log formats that are difficult to parse and
analyze programmatically.

12. Integration with Monitoring


Tools
Good Practice:
Link your logging with monitoring and alerting tools to automati-
cally detect anomalies and notify the concerned teams.
Avoid Practice:
Ignoring the integration of logs with monitoring systems can delay
the detection of issues.
Effective Logging with SLF4J and Logback 35

13. Log Aggregation


Good Practice:
In distributed environments, use centralized log aggregation to
collect logs from multiple services, simplifying analysis and cor-
relation of events.
Avoid Practice:
Allowing logs to remain scattered across various systems, compli-
cates the troubleshooting process.

14. Smart Logging


We have great content here for implementing Smart Logging using
AOP.
Best Practices for
Handling Exceptions
Java exception handling is an absolute must if you want to develop
reliable applications. Neglecting to handle exceptions properly can
cause instability and ultimately ruin the user’s experience espe-
cially when dealing with third-party services and Microservices.
But don’t worry, we’ve got you covered! This article will show you
how to handle exceptions like a Pro.

Understanding the Exception


Hierarchy
Java’s exception hierarchy is designed to categorize and handle dif-
ferent types of exceptions effectively. It is essential to understand
this hierarchy when designing exception-handling mechanisms
for a health management system. The two main categories of
exceptions are:

1. Checked Exceptions: These exceptions represent


anticipated error conditions that can occur during the
normal execution of the program. They are typically
recoverable and require explicit handling or declaration
in the method signature. Examples include IOException,
SQLException, and custom exceptions specific to the health
management domain, such as PatientNotFoundException
or MedicalRecordAccessException. Checked exceptions
ensure that the developer is aware of potential issues and
Best Practices for Handling Exceptions 37

can implement appropriate error handling and recovery


mechanisms.
2. Unchecked Exceptions: These exceptions indicate
programming errors or unexpected runtime conditions.
They are subclasses of RuntimeException and do not
require explicit handling or declaration. Examples include
NullPointerException, ArrayIndexOutOfBoundsException,
and IllegalArgumentException. Unchecked exceptions
often signify bugs or improper usage of APIs and should be
prevented through careful programming practices and input
validation.

Understanding the distinction between checked and unchecked


exceptions allows developers to design exception-handling strate-
gies that align with the nature of the exceptions and the specific
requirements of the health management system.

Java Exception Practices:


• Catch Specific Exceptions
• Avoid Swallowing Exceptions
• Utilize Finally Blocks or Try-With-Resources
• Document Exceptions
• Avoid Using Exceptions for Flow Control
• Throw Specific and Meaningful Exceptions
• Prefer Checked Exceptions for Recoverable Conditions
• Wrap Exceptions When Appropriate
• Log Exceptions with Relevant Details
• Handle Exceptions at the Appropriate Layer

Now let’s dive into all of them:


Best Practices for Handling Exceptions 38

1. Catch Specific Exceptions


When catching exceptions, it is recommended to catch the most
specific exception type possible. This practice enables precise
error handling and avoids masking potential bugs or unintended
behavior.
Bad Practice:

1 try {
2 patientService.updatePatientRecord(patientRecord);
3 } catch (Exception e) {
4 // Generic exception handling
5 // Masks specific errors and hinders effective debuggin\
6 g
7 }

Good Practice:

1 try {
2 patientService.updatePatientRecord(patientRecord);
3 } catch (PatientNotFoundException e) {
4 // Handle specific exception when patient record is not\
5 found
6 // Log and propagate the exception or perform necessary\
7 recovery steps
8 } catch (DatabaseAccessException e) {
9 // Handle specific exception related to database access\
10 issues
11 // Implement appropriate error handling and recovery me\
12 chanisms
13 }

By catching specific exceptions, developers can provide targeted


error handling, improve code clarity, and facilitate effective debug-
ging and maintenance.
Best Practices for Handling Exceptions 39

2. Avoid Swallowing Exceptions


Swallowing exceptions, i.e., catching an exception without properly
handling or logging it, is a dangerous practice that can lead to silent
failures and difficult-to-diagnose issues. In a health management
system, ignoring exceptions can have severe consequences, such as
data inconsistencies or improper patient care.
Bad Practice:

1 try {
2 prescriptionService.fillPrescription(prescription);
3 } catch (DrugNotFoundException e) {
4 // Swallowing the exception without proper handling or \
5 logging
6 // Potential issues remain unnoticed and unresolved
7 }

Good Practice:

1 try {
2 prescriptionService.fillPrescription(prescription);
3 } catch (DrugNotFoundException e) {
4 // Log the exception with relevant details
5 logger.error("Failed to fill prescription. Drug not fou\
6 nd: {}", prescription.getDrugId(), e);
7
8 // Propagate the exception or perform necessary error h\
9 andling
10 throw new PrescriptionFillException("Failed to fill pre\
11 scription", e);
12 }

By logging exceptions and properly handling or propagating them,


developers can ensure that errors are visible, traceable, and ad-
dressed promptly.
Best Practices for Handling Exceptions 40

3. Utilize Finally Blocks or


Try-With-Resources
Resource management is critical, especially when dealing with
external resources such as file handles, database connections, or
network sockets. Proper resource cleanup ensures that resources
are released promptly, preventing resource leaks and maintaining
system stability.
Bad Practice:

1 BufferedReader reader = null;


2 try {
3 reader = new BufferedReader(new FileReader("patient_dat\
4 a.txt"));
5 // Process patient data
6
7 } catch (IOException e) {
8 // Exception handling
9
10 } finally {
11 if (reader != null) {
12 try {
13 reader.close();
14 } catch (IOException e) {
15 // Exception handling during resource cleanup
16 }
17 }
18 }

Good Practice:
Best Practices for Handling Exceptions 41

1 try (BufferedReader reader = new BufferedReader(new FileR\


2 eader("patient_data.txt"))) {
3 // Process patient data
4
5 } catch (IOException e) {
6 // Exception handling
7 }

By using try-with-resources (available since Java 7), the resource is


automatically closed when the try block exits, eliminating the need
for explicit cleanup code in the finally block. This approach ensures
proper resource handling and reduces the likelihood of resource
leaks.

4. Document Exceptions
Documenting exceptions is crucial for maintaining a clear and
maintainable codebase. By providing meaningful and accurate
documentation, developers can communicate the expected behav-
ior of methods, the exceptions they may throw, and any precondi-
tions or postconditions.
Good Practice:

1 /**
2 * Updates the patient's health record with the provided \
3 information.
4 *
5 * @param record The health record to update.
6 * @throws PatientNotFoundException if the patient record\
7 does not exist.
8 * @throws DatabaseAccessException if there is an error a\
9 ccessing the database.
10 */
Best Practices for Handling Exceptions 42

11 public void updateHealthRecord(HealthRecord record) throw\


12 s PatientNotFoundException, DatabaseAccessException {
13 // Implementation
14 }

Documenting exceptions using Javadoc or inline comments helps


other developers understand the possible exceptional conditions
and how to handle them appropriately. It promotes code clarity,
maintainability, and collaboration among team members.

5. Avoid Using Exceptions for Flow


Control
Exceptions should not be used as a means of normal flow control
in a program. Overusing exceptions for non-exceptional scenarios
can lead to complex and hard-to-understand code, reduced perfor-
mance, and decreased maintainability.
Bad Practice:

1 try {
2 return patientList.get(patientId);
3 } catch (IndexOutOfBoundsException e) {
4 return null;
5 }

Good Practice:

1 if (patientId >= 0 && patientId < patientList.size()) {


2 return patientList.get(patientId);
3 }
4 return null; // or throw a specific exception if necessary
Best Practices for Handling Exceptions 43

By using conditional statements and proper input validation, devel-


opers can handle expected situations without relying on exceptions.
Exceptions should be reserved for truly exceptional scenarios that
disrupt the normal flow of the program.

6. Throw Specific and Meaningful


Exceptions
When throwing exceptions, it is important to use specific and
meaningful exception types that accurately represent the nature
of the error or exceptional condition. Generic exceptions like
‘Exception‘ or ‘RuntimeException‘ provide little information about
the cause of the problem and make it harder to handle exceptions
appropriately.
Bad Practice:

1 public void savePatientData(PatientData data) throws Exce\


2 ption {
3 // Throwing a generic exception
4 // Lacks specificity and hinders effective exception ha\
5 ndling
6 }

Good Practice:

1 public void savePatientData(PatientData data) throws Data\


2 PersistenceException {
3 // Throwing a specific exception related to data persis\
4 tence
5 // Provides clear indication of the nature of the probl\
6 em
7 }
Best Practices for Handling Exceptions 44

By throwing specific exceptions, developers can convey the precise


error condition, making it easier to handle and diagnose issues.
Custom exception classes can be created to represent domain-
specific exceptional scenarios, providing additional context and
facilitating targeted exception handling.

7. Prefer Checked Exceptions for


Recoverable Conditions
Checked exceptions are suited for representing recoverable error
conditions that the caller is expected to handle. They enforce
explicit exception handling and make the exceptional conditions
visible in the method signature.
Bad Practice:

1 public void calculateDosage(Patient patient) {


2 if (patient.getWeight() <= 0) {
3 throw new IllegalArgumentException("Patient weight mu\
4 st be positive");
5 }
6 // Dosage calculation logic
7 }

Good Practice:

1 public void calculateDosage(Patient patient) throws Inval\


2 idPatientWeightException {
3 if (patient.getWeight() == 0) {
4 throw new InvalidPatientWeightException("Patient weig\
5 ht cannot be zero.");
6 }
7 }
Best Practices for Handling Exceptions 45

8. Wrap Exceptions When


Appropriate
When propagating exceptions across different layers or modules of
the application, it may be necessary to wrap the original exception
inside a more appropriate exception type. Wrapping exceptions
allows for providing additional context, hiding implementation
details, and presenting a consistent exception interface to the caller.
Good Practice:

1 public void retrievePatientHistory() throws PatientDataAc\


2 cessException {
3 try {
4 // Retrieve patient history from the database
5 // ...
6
7 } catch (SQLException e) {
8 // Wrap the SQLException in a more appropriate except\
9 ion type
10 throw new PatientDataAccessException("Failed to retri\
11 eve patient history", e);
12 }
13 }

Wrapping exceptions helps maintain a clean and abstracted ex-


ception hierarchy, encapsulating the underlying implementation
exceptions and providing more meaningful exceptions to the higher
layers of the application.
Best Practices for Handling Exceptions 46

9. Log Exceptions with Relevant


Details
Logging exceptions is crucial for monitoring, debugging, and trou-
bleshooting issues in a health management system. When logging
exceptions, it is important to include relevant details such as
the exception message, stack trace, and any additional contextual
information that can aid in problem resolution.
Good Practice:

1 try {
2 // Perform database operation
3 // ...
4
5 } catch (SQLException e) {
6 // Log the exception with relevant details
7 logger.error("Database operation failed. Patient ID: {}\
8 , Operation: {}", patientId, operation, e);
9
10 // Rethrow the exception or handle it appropriately
11 throw new DatabaseAccessException("Failed to perform da\
12 tabase operation", e);
13 }

By logging exceptions with meaningful details, developers and


support teams can quickly identify and diagnose issues, reducing
the time required for problem resolution and improving the overall
maintainability of the system.
Best Practices for Handling Exceptions 47

10. Handle Exceptions at the


Appropriate Layer
Exception handling should be performed at the appropriate layer
of the application, based on the responsibility and scope of each
layer. The goal is to handle exceptions at a level where they can be
effectively managed and where appropriate actions can be taken.
Good Practice:

1 // Data Access Layer


2 public Patient getPatientById(int patientId) throws Patie\
3 ntNotFoundException {
4 try {
5 // Retrieve patient from the database
6 // ...
7
8 } catch (SQLException e) {
9 // Log the exception and throw a specific exception
10 logger.error("Failed to retrieve patient from the dat\
11 abase. Patient ID: {}", patientId, e);
12 throw new PatientNotFoundException("Patient not found\
13 with ID: " + patientId);
14 }
15 }
16
17 // Service Layer
18 public void updatePatientProfile(Patient patient) throws \
19 ProfileUpdateException {
20 try {
21 // Perform business logic and update patient profile
22 // ...
23 patientRepository.updatePatient(patient);
24
25 } catch (PatientNotFoundException e) {
Best Practices for Handling Exceptions 48

26 // Handle the exception thrown by the data access lay\


27 er
28 logger.warn("Patient not found while updating profile\
29 . Patient ID: {}", patient.getId(), e);
30 throw new ProfileUpdateException("Failed to update pa\
31 tient profile", e);
32 }
33 }

In this example, the data access layer handles the low-level SQLEx-
ception and throws a more specific PatientNotFoundException.
The service layer catches the PatientNotFoundException and takes
appropriate action, such as logging a warning and throwing a
higher-level ProfileUpdateException.
By handling exceptions at the appropriate layer, the system can
provide more meaningful error messages, maintain a clear separa-
tion of concerns, and ensure that exceptions are handled at a level
that can be effectively managed.
Chapter 6 - Best Practices
for Handling Database
Hello Java developers, accessing the Database is crucial but what
are the best practices? In this article, we will dive into the
Java database best practices that are essential for every devel-
oper aiming to master database interactions and ORM (Object-
Relational Mapping) in Java. By adopting these best practices,
you’ll enhance not only the performance of your applications
but also their maintainability. We’ll cover the key techniques
and practices such as abstraction, transaction management, lazy
loading, and null handling, which will help you effectively manage
common challenges in database access and ORM (Object-Relational
Mapping).

1. Using Repository Abstraction


When using frameworks such as Spring, Micronaut, or Quarkus
it is recommended to use the Data repository interfaces, such
as JpaRepository, which allows for a more abstract approach to
database access, leading to cleaner and more maintainable appli-
cation code.
Good: Interface-based repositories enable a cleaner separation of
concerns

1 public interface UserRepository extends JpaRepository<Use\


2 r, Long> {
3 Optional <User> findByEmail(String email);
4 }
Chapter 6 - Best Practices for Handling Database 50

1 @Service
2 public class UserService {
3 private final UserRepository userRepository;
4
5 @Autowired
6 public UserService(UserRepository userRepository) {
7 this.userRepository = userRepository;
8 }
9
10 public User getUserByEmail(String email) {
11 return userRepository.findByEmail(email)
12 .orElseThrow(() -> new UserNotFoundExcept\
13 ion("User not found"));
14 }
15 }

Explanation: This is good because it uses the framework Data’s


repository abstraction to create a query method that is clear and
concise. The findByEmail method follows the framework Data JPA
conventions and automatically provides the implementation. The
use of Optional is a good practice to avoid null checks and to handle
the absence of a result in a clean, expressive way.
Avoid: Direct use of EntityManager results in error-prone and less
manageable code

1 @Service
2 public class UserService {
3
4 @Autowired
5 private EntityManager entityManager;
6
7 public User getUserByEmail(String email) {
8 // Bad: Using EntityManager directly for somethin\
9 g repositories could do
10 Query query = entityManager.createQuery("SELECT u\
Chapter 6 - Best Practices for Handling Database 51

11 FROM User u WHERE u.email = :email");


12
13 query.setParameter("email", email);
14 return (User) query.getSingleResult();
15 }
16
17 }

Explanation: This is bad because it bypasses the simplicity and


safety provided by repository abstractions. Direct use of Entity-
Manager for this type of query is unnecessary and verbose. It also
opens up the potential for JPQL injection if not handled properly
and the method does not handle the case when the result is not
found, potentially throwing a NoResultException.
##2. Transaction Management
Good: Proper use of @Transactional at the service layer

1 @Service
2 @Transactional
3 public class UserService {
4 private final UserRepository userRepository;
5
6 public UserService(UserRepository userRepository) {
7 this.userRepository = userRepository;
8 }
9
10 public User createUser(User user) {
11 // The transaction is automatically managed by Sp\
12 ring
13 return userRepository.save(user);
14 }
15 }

Explanation: This is good because it uses the @Transactional anno-


tation at the service level, which is the recommended way to handle
Chapter 6 - Best Practices for Handling Database 52

transactions in a Spring application. This ensures that the entire


method execution is wrapped in a transactional context, which
provides consistency and integrity in the database operations.
Avoid: Annotating individual repository methods with @Transac-
tional is unnecessary

1 public interface UserRepository extends JpaRepository<Use\


2 r, Long> {
3 @Transactional
4 <extends User S> save(S entity);
5 }

Explanation: This is bad because adding @Transactional to a


method in a repository interface is not necessary since Spring Data
repositories are already transactional by nature. Moreover, this can
lead to confusion, as transaction management should typically be
handled at the service layer, not the repository layer. The service
layer often represents business transactions that can span multiple
repository calls, requiring a broader transactional context.

3. Handling Lazy Initialization


Good: Load lazy associations within a transaction using Hiber-
nate.initialize()
Chapter 6 - Best Practices for Handling Database 53

1 @Service
2 public class UserService {
3 private final UserRepository userRepository;
4
5 @Transactional(readOnly = true)
6 public User getUserWithOrders(Long userId) {
7 User user = userRepository.findById(userId)
8 .orElseThrow(() - new UserNotFoundExcepti\
9 on("User not found"));
10 // Initialize lazy collection
11 Hibernate.initialize(user.getOrders());
12 return user;
13 }
14 }

Explanation: This is good because it handles the lazy loading within


a transactional context, ensuring that the Hibernate.initialize()
method can load the lazy collection before the session is closed.
The use of readOnly = true is also good practice for read operations
as it can optimize performance.
Avoid: Accessing a lazily loaded collection outside a transaction
can lead to exceptions

1 @Service
2 public class UserService {
3 @Autowired
4 private UserRepository userRepository;
5
6 public User getUserWithOrders(Long userId) {
7 User user = userRepository.findById(userId).get();
8
9 // May throw LazyInitializationException
10 int orderCount = user.getOrders().size();
11
12 return user;
Chapter 6 - Best Practices for Handling Database 54

13 }
14 }

Explanation: This is bad because it attempts to access a lazily


loaded collection outside of an open session, which can result in
a LazyInitializationException. There’s no @Transactional anno-
tation, meaning that the session may be closed before the lazy
collection is accessed.

4. Using Pagination
Good: Implementing pagination with Spring Data’s Pageable Pag-
ination helps in fetching data in manageable chunks, thus saving
resources.

1 public interface UserRepository extends JpaRepository<Use\


2 r, Long> {
3 PageUser findAll(Pageable pageable);
4 }
5
6 @Service
7 public class UserService {
8 private final UserRepository userRepository;
9
10 public Page<User> getUsersWithPagination(int page, in\
11 t size) {
12 Pageable pageable = PageRequest.of(page, size, So\
13 rt.by("lastName").ascending());
14 return userRepository.findAll(pageable);
15 }
16 }

Explanation: This is good practice because it takes advantage of


Spring Data’s built-in support for pagination, which is important
Chapter 6 - Best Practices for Handling Database 55

for performance and usability when dealing with large datasets.


The Pageable parameter encapsulates pagination information and
sorting criteria, which the repository infrastructure uses to generate
the correct query.
Avoid: Retrieving all entries can lead to memory and performance
issues

1 @RestController
2 public class UserController {
3 private final UserRepository userRepository;
4
5 @GetMapping("/users")
6 public <List> User getAllUsers() {
7 }
8 }

5. Handling Null Values with


Optional
Check out here for more details dealing with null values.
Good: Using orElseThrow() to handle absent values elegantly

1 @Service
2 public class UserService {
3 private final UserRepository userRepository;
4
5 public UserService(UserRepository userRepository) {
6 this.userRepository = userRepository;
7 }
8
9 public User getUserById(Long id) {
10 return userRepository.findById(id)
Chapter 6 - Best Practices for Handling Database 56

11 .orElseThrow(() -> new UserNotFoundExcept\


12 ion("User with id " + id + " not found"));
13 }
14 }

Explanation: This is good because it makes use of Optional, a


feature introduced in Java 8, which is designed to provide a better
alternative to null. It forces the developer to think about the case
when the User might not be found and handle it accordingly,
possibly throwing a custom exception. Avoid: Using get() without
checking if the value is present can cause exceptions

1 @Service
2 public class UserService {
3 private final UserRepository userRepository;
4
5 public User getUserById(Long id) {
6 return userRepository.findById(id).get();
7 }
8 }

Explanation: This is bad because it assumes that the findById


method will always return a non-null value, which is not guaran-
teed. Using get() directly on the Optional returned by findById may
throw a NoSuchElementException if the Optional is empty (i.e. if
the user is not found). This approach fails to handle the potential
absence of a User with the given ID in a clean, safe manner.

Summary

The “Good” examples follow the best practices of Java framework


Data JPA and Java, promoting code readability, maintainability,
and proper error handling. In contrast, the “Avoid” examples
Chapter 6 - Best Practices for Handling Database 57

show common pitfalls that can lead to bugs, inefficient database


operations, and code that is harder to maintain and understand.

Appendix

A. Additional Resources

Books

• “Effective Java” by Joshua Bloch: A comprehensive guide


to writing robust, maintainable, and efficient Java code.

B. Tools and Libraries

Logging

• Logback1 : A powerful and flexible logging framework for


Java applications.
• SLF4J2 : A simple facade for logging frameworks, allowing
the end user to plug in the desired logging framework at
deployment time.

Exception Handling

• Vavr3 : Functional programming library for Java that provides


immutable collections and functional control structures.
• Exception handling like a Pro4 : Built-in support for han-
dling exceptions in Spring Boot applications.

1 https://logback.qos.ch/
2 http://www.slf4j.org/
3 https://github.com/vavr-io/vavr/
4 https://mezocode.com/exception-handling-in-java-like-a-pro/
Chapter 6 - Best Practices for Handling Database 58

API Design

• Swagger/OpenAPI5 : A powerful toolkit for API design and


documentation.
• Postman6 : An API platform for building and using APIs,
enabling easier testing and collaboration.
• RESTful web API design7 : An API platform for building and
using APIs, enabling easier testing and collaboration.

C. Code Samples and Templates

For more coding examples refer to my GitHub repo8

5 https://swagger.io/
6 https://www.postman.com/
7 https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design
8 https://github.com/mezocode

You might also like