best-java-microservices-coding-practices
best-java-microservices-coding-practices
Boost Microservices
Productivity
mezocde
This book is for sale at
http://leanpub.com/best-java-microservices-coding-practices
© 2024 mezocde
Contents
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
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
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 }
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 }
1 @RestController
2 @RequestMapping("/api/v1/users")
3 public class UserController {
4 // RESTful API actions for version 1
5 }
1 @RestController
2 @RequestMapping("/users")
3 public class UserController {
4 // API actions with no versioning.
5 }
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 }
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 }
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 }
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 }
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 }
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
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 }
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
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 }
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
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 }
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
1 if (user != null) {
2 user.updateProfile();
3 }
or suspicious activities.
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 }
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
Avoid Practice:
Logging everything at the same level, can overwhelm the log files
with noise and make it difficult to spot critical issues.
Avoid Practice:
Vague or generic log messages that do not provide sufficient context
to understand the event or issue.
Avoid Practice:
Concatenating strings within log statements is less efficient.
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 }
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.
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.
Avoid Practice:
Excessive logging at a high granularity in production, can lead to
performance issues and log flooding.
14
15 logger.info("Order processed successfully");
16 logger.trace("Exiting processOrder method");
17 }
Avoid Practice:
Logging sensitive information such as passwords, API keys, Credit
Cards, or personally identifiable information (PII).
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>
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.
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 }
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 }
Good Practice:
Best Practices for Handling Exceptions 41
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
1 try {
2 return patientList.get(patientId);
3 } catch (IndexOutOfBoundsException e) {
4 return null;
5 }
Good Practice:
Good Practice:
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 }
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 @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 }
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
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 }
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 }
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 }
4. Using Pagination
Good: Implementing pagination with Spring Data’s Pageable Pag-
ination helps in fetching data in manageable chunks, thus saving
resources.
1 @RestController
2 public class UserController {
3 private final UserRepository userRepository;
4
5 @GetMapping("/users")
6 public <List> User getAllUsers() {
7 }
8 }
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
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 }
Summary
Appendix
A. Additional Resources
Books
Logging
Exception Handling
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
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