Introduction
In the fast-paced modern environment for software development, handling multiple services performing similar functionalities across different projects can be daunting. Decreasing the redundancy of services and having the same performance and reliability is desired. We have developed an SBLS—a ‘Standard Base Line Service’—for currency conversions by using Docker, Spring Boot, and the circuit breaker pattern. This blog post will take a developer through creating a standard service image that can be reused throughout projects and allowing scalability enhancements to be added via a sidecar.
Use Case
This company faces a common problem: many internal projects have their implementations of services that perform almost identical tasks. This redundancy leads to increased maintenance efforts and consistency issues. To streamline our services, we aim to create a standard base service for currency conversion. This service will:
- Perform basic currency conversion.
- Call a sidecar service for premium conversion rates if a specific flag is set.
Exploring Possible Solutions
Monolithic Approach: Integrate all functionalities into a single service. This straightforward approach leads to a large, unwieldy codebase that requires more effort to maintain and scale.
Microservices without Docker: Separate the functionalities into distinct microservices. While this offers better modularity, managing dependencies and deployment can become complex without containerization.
Microservices with Docker: Use Docker to containerize services, providing isolated environments and simplifying deployment. This approach is flexible, scalable, and manageable.
Why REST with Docker?
After evaluating the options, we decided on a REST-based microservices architecture using Docker. Docker provides:
- Isolation: Each service runs in its container, ensuring consistency across environments.
- Scalability: Services can be scaled independently based on demand.
- Ease of Deployment: Docker simplifies deployment, reducing errors and streamlining operations.
Full Solution
Let’s dive into the solution, which involves creating a base currency conversion service and a sidecar service for premium conversion rates. We’ll use Java 17, Spring Boot 3.2, and Docker, integrating Resilience4j for circuit breaker functionality to enhance resilience.
Understanding the Sidecar Pattern and Its Implementation
What is the Sidecar Pattern?
The sidecar pattern is a design pattern used in microservices architecture where a secondary service (the sidecar) runs alongside the primary application service. This secondary service can provide various auxiliary functionalities such as logging, monitoring, or, as in our case, extended business logic.
The primary service (SBLS) and the sidecar service share the same lifecycle, meaning they are deployed and scaled together. The sidecar pattern helps keep the primary service simple and focused on its core functionality while the sidecar handles additional features.
Why Use the Sidecar Pattern?
Separation of Concerns: The sidecar pattern separates the primary business logic from auxiliary or extended functionalities, making the primary service easier to maintain and understand.
Modularity: It allows for modular development. Different teams can work independently on the primary and sidecar services, enhancing productivity and reducing complexity.
Flexibility: The sidecar can be easily updated, replaced, or scaled without affecting the primary service, allowing for greater flexibility and resilience.
Enhanced Capabilities: In our use case, the sidecar provides a premium currency conversion rate, extending the capabilities of the SBLS without altering its core functionality.
Using an Environment Variable for the Sidecar Service
We use an environment variable (USE_SIDECAR) to control whether the sidecar service should be utilized. This approach provides several benefits:
Dynamic Configuration: An environment variable allows you to dynamically configure the service behavior without changing the code. This is particularly useful in deployment environments (development, testing, production).
Feature Toggle: The environment variable acts as a feature toggle, allowing you to easily enable or disable the sidecar service. This is helpful for A/B testing, gradual rollouts, or the sidecar service is temporarily unavailable.
Simplicity: It simplifies the deployment process. You can use the same Docker image and change the environment variable to alter the service behavior, avoiding the need to build and maintain multiple image versions.
Implementation in Our Solution
In our currencyconversion service, we check the USE_SIDECAR environment variable to decide whether to call the sidecar service for premium currency conversion rates. Here’s a snippet from our CurrencyConversionService:
@Service
public class CurrencyConversionService {
...
@Value("${premium.rate.url}")
private String premiumRateUrl;
@Value("${use.sidecar:false}")
private boolean useSidecar;
...
By incorporating this pattern, we achieve a flexible, modular, and resilient architecture that simplifies maintenance and enhances functionality without complicating the core service.
This next section should help in understanding the rationale behind using the sidecar pattern and the environment variable for flagging its usage in our solution.
Step 1: Create the Currency Conversion Service (SBLS) and Sidecar Service Directory Structure
Directory Structure
projects/
├── currency-conversion-service/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── com/
│ │ │ │ └── example/
│ │ │ │ └── currencyconversion/
│ │ │ │ ├── CurrencyConversionServiceApplication.java
│ │ │ │ ├── controller/
│ │ │ │ │ └── CurrencyConversionController.java
│ │ │ │ └── service/
│ │ │ │ └── CurrencyConversionService.java
│ │ │ └── resources/
│ │ │ └── application.properties
│ ├── Dockerfile
│ └── pom.xml
└── premium-rate-service/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── premiumrate/
│ │ │ ├── PremiumRateServiceApplication.java
│ │ │ ├── controller/
│ │ │ │ └── PremiumRateController.java
│ │ └── resources/
│ │ └── application.properties
├── Dockerfile
└── pom.xml
docker-compose.yml
Step 2: Create the Currency Conversion (SBLS) and Sidecar Services
CurrencyConversionServiceApplication.java
package com.example.currencyconversion;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CurrencyConversionServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CurrencyConversionServiceApplication.class, args);
}
}
CurrencyConversionController.java
package com.example.currencyconversion.controller;
import com.example.currencyconversion.service.CurrencyConversionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/convert")
public class CurrencyConversionController {
@Autowired
private CurrencyConversionService conversionService;
@GetMapping
public double convertCurrency(@RequestParam double amount, @RequestParam String from, @RequestParam String to, @RequestParam boolean usePremiumRate) {
return conversionService.convertCurrency(amount, from, to, usePremiumRate);
}
}
CurrencyConversionService.java
package com.example.currencyconversion.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class CurrencyConversionService {
@Value("${premium.rate.url}")
private String premiumRateUrl;
@Value("${use.sidecar:false}")
private boolean useSidecar;
private final RestTemplate restTemplate = new RestTemplate();
public double convertCurrency(double amount, String from, String to, boolean usePremiumRate) {
double basicRate = getBasicRate(from, to);
if (usePremiumRate && useSidecar) {
double premiumRate = getPremiumRate(from, to);
return amount * premiumRate;
}
return amount * basicRate;
}
private double getBasicRate(String from, String to) {
// Basic conversion rate logic
return 1.1; // Placeholder
}
@CircuitBreaker(name = "premiumRateCircuitBreaker", fallbackMethod = "fallbackPremiumRate")
private double getPremiumRate(String from, String to) {
String url = premiumRateUrl + "?from=" + from + "&to=" + to;
return restTemplate.getForObject(url, Double.class);
}
public double fallbackPremiumRate(String from, String to, Throwable throwable) {
// Fallback logic if premium rate service fails
return 1.15; // Placeholder for fallback rate
}
}
application.properties
# Services Properties
premium.rate.url=http://premium-rate-service:8081/rate
use.sidecar=${USE_SIDECAR:false}
# Circuit Breaker Properties
resilience4j.circuitbreaker.instances.premiumRateCircuitBreaker.slidingWindowSize=10
resilience4j.circuitbreaker.instances.premiumRateCircuitBreaker.permittedNumberOfCallsInHalfOpenState=3
resilience4j.circuitbreaker.instances.premiumRateCircuitBreaker.minimumNumberOfCalls=5
resilience4j.circuitbreaker.instances.premiumRateCircuitBreaker.failureRateThreshold=50
resilience4j.circuitbreaker.instances.premiumRateCircuitBreaker.waitDurationInOpenState=10000
Dockerfile
FROM openjdk:17-jdk-slim
VOLUME /tmp
COPY target/currency-conversion-service.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
PremiumRateServiceApplication.java
package com.example.premiumrate;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PremiumRateServiceApplication {
public static void main(String[] args) {
SpringApplication.run(PremiumRateServiceApplication.class, args);
}
}
PremiumRateController.java
package com.example.premiumrate.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/rate")
public class PremiumRateController {
@GetMapping
public double getPremiumRate(@RequestParam String from, @RequestParam String to) {
// Premium conversion rate logic
return 1.2; // Placeholder
}
}
application.properties
# Add any required properties here
Dockerfile
FROM openjdk:17-jdk-slim
VOLUME /tmp
COPY target/premium-rate-service.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Step 3: Create Combined Docker Compose File
Create a combined docker-compose.yml file to deploy both services.
docker-compose.yml
version: '3.8'
services:
currency-conversion-service:
build:
context: ./currencyconversion
dockerfile: Dockerfile
image: currencyconversion
environment:
- PREMIUM_RATE_URL=http://premium-rate-service:8081/rate
- USE_SIDECAR=true
networks:
mynetwork:
ipv4_address: 172.25.0.2
ports:
- "12080:8080"
depends_on:
- premium-rate-service
premium-rate-service:
build:
context: ./premiumrate
dockerfile: Dockerfile
image: premiumrate
networks:
mynetwork:
ipv4_address: 172.25.0.3
ports:
- "8081:8081"
networks:
mynetwork:
driver: bridge
ipam:
config:
- subnet: 172.25.0.0/16
Step 4: Build and Deploy Services
- Build the
currencyconversionservice
cd projects/currencyconversion
mvn clean package
docker build -t currencyconversion .
- Build the
premiumrateservice
cd projects/premiumrate
mvn clean package
docker build -t premiumrate .
- Run Docker Compose for both services
cd projects
docker-compose up
Step 5: Test the Services
Use Postman to test the currency-conversion-service:
- Endpoint:
http://192.168.0.88:12080/convert?amount=100&from=USD&to=EUR&usePremiumRate=true - Response:
{ "result": 120.0 }if the sidecar is used. - Response:
{ "result": 110.0 }if the sidecar is not used.
Conclusion
Following these steps, you can create a resilient, scalable, and maintainable currency conversion service using Docker, Spring Boot, and Resilience4j. This setup demonstrates using the sidecar pattern to extend functionalities without modifying the base service, ensuring consistency and flexibility across your projects.
Feel free to reach out for any further assistance or questions regarding this implementation. Happy coding!
