Tuesday, November 28, 2023

Mastering Microservices: Navigating Common Pitfalls with Bad Code to Good Code Solutions - Part 2🛡️

  • Dive into the world of microservices with examples of bad code practices and their corresponding good code solutions. 
  • Each example focuses on common pitfalls encountered in microservice development, addressing issues such as performance, scalability, and maintainability. 
  • Learn how to optimize your microservices architecture by exploring practical scenarios, understanding the drawbacks of bad code, and implementing effective solutions to enhance the overall robustness of your applications.

Example 1: Synchronous File Upload in a Web Application

  • Problem: Fetching data inefficiently in a RESTful API can lead to performance issues, especially when dealing with large datasets.
  • Bad Code Example:
  • // Bad code: Synchronous file upload in a web application @WebServlet("/upload") public class FileUploadServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Part filePart = request.getPart("file"); // Process and save the file synchronously } }
  • Good Code Example:
  • // Good code: Asynchronous file upload in a web application @WebServlet("/upload") public class FileUploadServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Part filePart = request.getPart("file"); // Process and save the file asynchronously CompletableFuture.runAsync(() -> { // Asynchronously handle file processing }); } }
  • Implement asynchronous file processing to free up server threads and improve scalability which  Enhanced server scalability and responsiveness during file uploads.

Example 2:Lack of Circuit Breaker for Fault Tolerance

  • Problem: Lack of a circuit breaker can result in cascading failures if a dependent microservice is unavailable.
  • Bad Code Example:
  • // Bad code: Lack of circuit breaker for fault tolerance public class ProductService { @HystrixCommand public ProductDTO getProductDetails(Long productId) { // Call another microservice without circuit breaker return restTemplate.getForObject("http://product-service/api/products/{productId}", ProductDTO.class, productId); } }
  • Good Code Example:
  • // Good code: Circuit breaker for fault tolerance public class ProductService { @HystrixCommand(fallbackMethod = "fallbackProductDetails") public ProductDTO getProductDetails(Long productId) { // Call another microservice with circuit breaker return restTemplate.getForObject("http://product-service/api/products/{productId}", ProductDTO.class, productId); } public ProductDTO fallbackProductDetails(Long productId) { // Fallback mechanism to handle failures return new ProductDTO("Fallback Product", 0.0); } }
  • Implement a circuit breaker pattern (e.g., using Netflix Hystrix) for fault tolerance helps to Improved fault tolerance and resilience in microservice communication.

Example 3: Monolithic Database Access in Microservices

  • ProblemDirectly accessing a monolithic database from a microservice can lead to performance bottlenecks.
  • Bad Code Example:
  • // Bad code: Monolithic database access in microservices public class OrderService { @Autowired private OrderRepository orderRepository; public OrderDTO getOrderDetails(Long orderId) { // Accessing a monolithic database directly Order order = orderRepository.findById(orderId).orElse(null); // Convert Order to OrderDTO and return } }
  • Good Code Example:
  • // Good code: Microservices-specific database access public class OrderService { @Autowired private OrderRepository orderRepository; public OrderDTO getOrderDetails(Long orderId) { // Accessing a microservices-specific database Order order = orderRepository.findByOrderIdInMicroserviceDatabase(orderId); // Convert Order to OrderDTO and return } }
  • Implement a microservices-specific database, or use appropriate database sharding and partitioning strategies Enhanced database access performance in amicroservices architecture.

Example 4: Lack of Service Discovery

  • Problem: Hardcoding microservice URLs can lead to manual configuration errors and hinder dynamic scalability.
  • Bad Code Example:
  • // Bad code: Lack of service discovery in microservices public class OrderService { @Autowired private RestTemplate restTemplate; public OrderDTO getOrderDetails(Long orderId) { // Hardcoded microservice URL ResponseEntity<OrderDTO> responseEntity = restTemplate.exchange( "http://product-service/api/products/{orderId}", HttpMethod.GET, null, OrderDTO.class, orderId ); return responseEntity.getBody(); } }
  • Good Code Example:
  • // Good code: Service discovery in microservices public class OrderService { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; public OrderDTO getOrderDetails(Long orderId) { // Dynamically discover microservice URL ServiceInstance serviceInstance = discoveryClient.getInstances("product-service").get(0); String productUrl = serviceInstance.getUri() + "/api/products/{orderId}"; ResponseEntity<OrderDTO> responseEntity = restTemplate.exchange( productUrl, HttpMethod.GET, null, OrderDTO.class, orderId ); return responseEntity.getBody(); } }
  • Implement service discovery for dynamic microservice location helps to improved dynamic scalability and reduced configuration errors in microservices communication.

Example 5: Lack of Bulkhead Pattern Implementation

  • Problem: Lack of bulkhead pattern implementation can lead to increased risk of cascading failures.
  • Bad Code Example:
  • // Bad code: Lack of bulkhead pattern implementation in microservices public class OrderService { @Autowired private ProductService productService; public List<OrderDTO> getOrderDetailsForMultipleOrders(List<Long> orderIds) { // Synchronously call another microservice for each order List<OrderDTO> orderDTOList = new ArrayList<>(); for (Long orderId : orderIds) { OrderDTO orderDTO = productService.getProductDetails(orderId); orderDTOList.add(orderDTO); } return orderDTOList; } }
  • Good Code Example:
  • // Good code: Implementing bulkhead pattern in microservices public class OrderService { @Autowired private ProductService productService; @HystrixCommand(fallbackMethod = "fallbackOrderDetails") public List<OrderDTO> getOrderDetailsForMultipleOrders(List<Long> orderIds) { // Asynchronously call another microservice for each order with bulkhead pattern List<CompletableFuture<OrderDTO>> futures = orderIds.stream() .map(orderId -> CompletableFuture.supplyAsync(() -> productService.getProductDetails(orderId))) .collect(Collectors.toList()); return futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); } public List<OrderDTO> fallbackOrderDetails(List<Long> orderIds) { // Fallback mechanism to handle failures for bulkhead pattern return Collections.emptyList(); } }
  • Implement the bulkhead pattern to isolate failure points and enhance system resilience

Example 6: Lack of Retry Mechanism

  • Problem: Lack of a retry mechanism can lead to increased chances of transient failures affecting system performance.
  • Bad Code Example:
  • // Bad code: Lack of retry mechanism in microservices public class OrderService { @Autowired private ProductService productService; public OrderDTO getOrderDetails(Long orderId) { // Synchronously call another microservice without retry mechanism return productService.getProductDetails(orderId); } }
  • Good Code Example:
  • // Good code: Implementing retry mechanism in microservices public class OrderService { @Autowired private ProductService productService; @Retryable(value = {TransientException.class}, maxAttempts = 3, backoff = @Backoff(delay = 100)) public OrderDTO getOrderDetails(Long orderId) { // Asynchronously call another microservice with retry mechanism return productService.getProductDetails(orderId); } }
  • Implement a retry mechanism to handle transient failures gracefully which Improved handling of transient failures and increased system resilience.

Example 7: Lack of API Versioning

  • Problem: Lack of API versioning can lead to challenges in maintaining backward compatibility and evolving microservices.
  • Bad Code Example:
  • // Bad code: Lack of API versioning in microservices @RestController @RequestMapping("/api/orders") public class OrderController { @GetMapping("/{orderId}") public OrderDTO getOrderDetails(@PathVariable Long orderId) { // Endpoint without versioning // ... } }
  • Good Code Example:
  • // Good code: Implementing API versioning in microservices @RestController @RequestMapping("/api/v1/orders") public class OrderControllerV1 { @GetMapping("/{orderId}") public OrderDTO getOrderDetails(@PathVariable Long orderId) { // Endpoint for version 1 // ... } } @RestController @RequestMapping("/api/v2/orders") public class OrderControllerV2 { @GetMapping("/{orderId}") public OrderDTO getOrderDetails(@PathVariable Long orderId) { // Endpoint for version 2 // ... } }
  • Implement API versioning to provide a clear mechanism for evolving APIs which helpes to Improved maintainability and backward compatibility with clear API versioning.

Example 8: Inefficient Use of Blocking I/O in a Microservice

  • Problem: Using synchronous/blocking I/O can lead to decreased performance and scalability..
  • Bad Code Example:
  • // Bad code: Inefficient use of blocking I/O in a microservice @Service public class OrderService { @Autowired private RestTemplate restTemplate; public OrderDTO getOrderDetails(Long orderId) { // Synchronously call another microservice using RestTemplate return restTemplate.getForObject("http://product-service/api/products/{orderId}", OrderDTO.class, orderId); } }
  • Good Code Example:
  • // Good code: Using reactive programming for microservice communication @Service public class OrderService { @Autowired private WebClient webClient; public Mono<OrderDTO> getOrderDetails(Long orderId) { // Asynchronously call another microservice using WebClient return webClient.get() .uri("http://product-service/api/products/{orderId}", orderId) .retrieve() .bodyToMono(OrderDTO.class); } }
  • Implement reactive programming or use asynchronous patterns for non-blocking I/O helps to improved microservice performance and scalability with non-blocking I/O.

Example 9: Lack of Distributed Tracing

  • Problem: Lack of distributed tracing can make it challenging to identify and diagnose performance issues across microservices.
  • Bad Code Example:
  • // Bad code: Lack of distributed tracing in microservices @Service public class OrderService { @Autowired private RestTemplate restTemplate; public OrderDTO getOrderDetails(Long orderId) { // Synchronously call another microservice without distributed tracing return restTemplate.getForObject("http://product-service/api/products/{orderId}", OrderDTO.class, orderId); } }
  • Good Code Example:
  • // Good code: Implementing distributed tracing in microservices @Service public class OrderService { @Autowired private WebClient webClient; public Mono<OrderDTO> getOrderDetails(Long orderId) { // Asynchronously call another microservice with distributed tracing return webClient.get() .uri("http://product-service/api/products/{orderId}", orderId) .header("X-B3-TraceId", TraceContext.current().traceIdString()) // Propagate trace context .retrieve() .bodyToMono(OrderDTO.class); } }
  • Implement distributed tracing using tools like Zipkin or Jaeger which helps to enhanced diagnosis and resolution of performance issues in microservices.

Example 10: Lack of Rate Limiting

  • Problem: Lack of rate limiting can expose microservices to abuse and degradation of service quality..
  • Bad Code Example:
  • // Bad code: Lack of rate limiting in a microservice @RestController @RequestMapping("/api/orders") public class OrderController { @GetMapping("/{orderId}") public OrderDTO getOrderDetails(@PathVariable Long orderId) { // Endpoint without rate limiting // ... } }
  • Good Code Example:
  • // Good code: Implementing rate limiting in a microservice @RestController @RequestMapping("/api/orders") public class OrderController { @GetMapping("/{orderId}") @RateLimit(limit = 10, duration = Duration.ofMinutes(1)) public OrderDTO getOrderDetails(@PathVariable Long orderId) { // Endpoint with rate limiting // ... } }
  • Implement rate limiting to control the number of requests a microservice can handle helps to Protection against abuse and more controlled resource utilization

Example 11: Lack of Caching in Microservices

  • Problem: Lack of caching can result in repeated expensive operations, impacting microservice performance..
  • Bad Code Example:
  • // Bad code: Lack of caching in a microservice @Service public class OrderService { @Autowired private ProductRepository productRepository; public OrderDTO getOrderDetails(Long orderId) { // Fetch product details without caching Product product = productRepository.findByOrderId(orderId); // ... } }
  • Good Code Example:
  • // Good code: Implementing caching in a microservice @Service public class OrderService { @Autowired private ProductRepository productRepository; @Cacheable(value = "productCache", key = "#orderId") public OrderDTO getOrderDetails(Long orderId) { // Fetch product details with caching Product product = productRepository.findByOrderId(orderId); // ... } }
  • Implement caching to store and retrieve frequently accessed data which helpes to Improve microservice performance through efficient data caching.

Example 12: Without Design Pattern (Bad) - Service Configuration

  • Problem: Hardcoding configuration details within a service can lead to inflexibility and maintenance challenges.
  • Bad Code Example:
  • // Bad code: Without design pattern (Service Configuration) public class ProductService { private String databaseUrl; public ProductService() { this.databaseUrl = ConfigProvider.getConfig("database.url"); } // ... }
  • Good Code Example:
  • // Good code: Config server for service configuration public class ProductService { private String databaseUrl; public ProductService(ConfigServer configServer) { this.databaseUrl = configServer.getConfig("database.url"); } // ... }
  • Using a config server allows centralized configuration management, making it easier to update and manage service configurations.

Example 13: Without Design Pattern (Bad) - API Gateway

  • Problem: Direct client calls to individual services can lead to increased complexity in managing multiple service endpoints.
  • Bad Code Example:
  • // Bad code: Without design pattern (API Gateway) public class OrderService { public String getOrderDetails(String orderId) { // Implementation of getting order details // ... return "Order Details"; } } public class PaymentService { public String getPaymentDetails(String paymentId) { // Implementation of getting payment details // ... return "Payment Details"; } }
  • Good Code Example:
  • // Good code: API Gateway public class ApiGateway { private OrderService orderService; private PaymentService paymentService; public ApiGateway(OrderService orderService, PaymentService paymentService) { this.orderService = orderService; this.paymentService = paymentService; } public String getOrderDetails(String orderId) { // Forward request to OrderService return orderService.getOrderDetails(orderId); } public String getPaymentDetails(String paymentId) { // Forward request to PaymentService return paymentService.getPaymentDetails(paymentId); } }
  • Using an API Gateway consolidates client requests and provides a single entry point to interact with multiple services.

Example 14: Inefficient Looping

  • Problem: The list.size() method is called in each iteration, leading to unnecessary method calls and potential performance overhead.
  • Bad Code Example:
  • // Bad code: Inefficient looping for (int i = 0; i < list.size(); i++) { // Code inside loop }
  • Good Code Example:
  • // Good code: Enhanced for loop for (Object item : list) { // Code inside loop }

Example 15: Without Centralised Logging

  • Problem: Implementing logging directly in services can lead to scattered logs and limited log management capabilities.
  • Bad Code Example:
  • // Bad code: Without design pattern (Centralized Logging) public class OrderService { public void processOrder(String orderDetails) { // Implementation of processing an order // ... Logger.log("Order processed successfully"); } } public class PaymentService { public void processPayment(String paymentDetails) { // Implementation of processing a payment // ... Logger.log("Payment processed successfully"); } } public class Logger { public static void log(String message) { // Implementation of logging System.out.println(message); } }
  • Good Code Example:
  • // Good code: Centralized logging public class OrderService { private LoggerService loggerService; public OrderService(LoggerService loggerService) { this.loggerService = loggerService; } public void processOrder(String orderDetails) { // Implementation of processing an order // ... loggerService.log("Order processed successfully"); } } public class PaymentService { private LoggerService loggerService; public PaymentService(LoggerService loggerService) { this.loggerService = loggerService; } public void processPayment(String paymentDetails) { // Implementation of processing a payment // ... loggerService.log("Payment processed successfully"); } } public class LoggerService { public void log(String message) { // Implementation of centralized logging System.out.println(message); } }
  • Centralized logging ensures that logs from various services are aggregated, providing better traceability and management

Example 16:  Without Database Sharding strategy

  • Problem: Implementing database sharding without a standardized pattern can lead to inconsistent sharding logic and challenges in scaling..
  • Bad Code Example:
  • // Bad code: Without design pattern (Database Sharding) public class OrderService { private DatabaseShardSelector shardSelector; public OrderService(DatabaseShardSelector shardSelector) { this.shardSelector = shardSelector; } public Order getOrderById(String orderId) { // Select the database shard based on orderId String shard = shardSelector.selectShard(orderId); // Query the specific shard for the order // ... return new Order(orderId, shard); } } public class DatabaseShardSelector { public String selectShard(String key) { // Implementation of shard selection logic // ... return "shard1"; } }
  • Good Code Example:
  • // Good code: Database Sharding Pattern public class OrderService { private DatabaseShardingStrategy shardingStrategy; public OrderService(DatabaseShardingStrategy shardingStrategy) { this.shardingStrategy = shardingStrategy; } public Order getOrderById(String orderId) { // Use the sharding strategy to determine the database shard String shard = shardingStrategy.determineShard(orderId); // Query the specific shard for the order // ... return new Order(orderId, shard); } } public interface DatabaseShardingStrategy { String determineShard(String key); } public class DefaultShardingStrategy implements DatabaseShardingStrategy { @Override public String determineShard(String key) { // Implementation of default shard selection logic // ... return "shard1"; } }
  • Using a standardized database sharding pattern provides better consistency and scalability in managing distributed data.

You may also like

Kubernetes Microservices
Python AI/ML
Spring Framework Spring Boot
Core Java Java Coding Question
Maven AWS