Suppose that you have a webshop with a credit card payment option. Credit card payments work fine in a production environment. There is a new requirement to implement a sandbox environment for the webshop. Sandbox environment doesn’t charge your credit card. It always returns a successful response.
Each checkout implementation is based on the following CheckoutService interface:
1 2 3 |
public interface CheckoutService { public boolean checkout(Integer orderId, BigDecimal amount, String creditCardNumber); } |
The implementation of CheckoutService an interface for the production environment is BankCheckoutService
1 2 3 4 5 6 7 8 |
@Service public class BankCheckoutService implements CheckoutService { @Override public boolean checkout(Integer orderId, BigDecimal amount, String creditCardNumber) { System.out.println("charge credit card using real payment gateway"); return true; } } |
CheckoutController gets instance of CheckoutService using @Autowired annotation (dependency injection).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@RestController public class CheckoutController { private final CheckoutService checkoutService; @Autowired public CheckoutController(CheckoutService checkoutService) { this.checkoutService = checkoutService; } @GetMapping("/checkout") public ResponseEntity<Boolean> checkout() { Integer orderId = 12377; BigDecimal amount = new BigDecimal(20000.50); String creditCardNumber = "4111111111111111"; boolean success = checkoutService.checkout(orderId, amount, creditCardNumber); return ResponseEntity.ok(success); } } |
Let us create a new implementation of CheckoutService interface for the sandbox environment:
1 2 3 4 5 6 7 8 |
@Service public class SandboxCheckoutService implements CheckoutService { @Override public boolean checkout(Integer orderId, BigDecimal amount, String creditCardNumber) { System.out.println("don't charge credit card, always return successful response"); return true; } } |
It will work fine if we replace a constructor of CheckoutController to get SandboxCheckoutService bean:
1 2 3 4 |
@Autowired public CheckoutController(SandboxCheckoutService checkoutService) this.checkoutService = checkoutService; } |
However, we want to instantiate either BankCheckoutService or SandboxCheckoutService based on the variable in the application properties file. SandboxCheckoutService should be instantiated if application properties contains the following content:
1 |
sandbox=true |
If sandbox the variable is false BankCheckoutService will be instantiated.
Spring Boot already has a solution. @ConditionalOnProperty annotation provides us the possibility to conditionally instantiate bean based on property value from application.properties file.
BankCheckoutService:
1 2 3 4 5 6 7 8 9 |
@Service @ConditionalOnProperty(name = "sandbox", havingValue = "false") public class BankCheckoutService implements CheckoutService { @Override public boolean checkout(Integer orderId, BigDecimal amount, String creditCardNumber) { System.out.println("charge credit card using real payment gateway"); return true; } } |
SandboxCheckoutService
1 2 3 4 5 6 7 8 9 |
@Service @ConditionalOnProperty(name = "sandbox", havingValue = "true") public class SandboxCheckoutService implements CheckoutService { @Override public boolean checkout(Integer orderId, BigDecimal amount, String creditCardNumber) { System.out.println("don't charge credit card, always return successful response"); return true; } } |
The controller doesn’t have to be changed since it expects CheckoutService interface which is implemented in both BankCheckoutService and SandboxCheckoutService. Controller depends on abstraction not implementation as it is specified in SOLID principle.
We can now switch between BankCheckoutService and SandboxCheckoutService by changing the sandbox variable in the application.properties file.