If you have ever worked on a billing system, you know the nightmare: every insurance provider has different rules. Some cover 80%, some have fixed discounts, and some cover nothing at all. Before you know it, your code is a mountain of nested if-else or switch statements that are impossible to test and maintain.
The project I am referencing: Hospital Information System
That is exactly the problem I solved in my Hospital Information System using the Strategy Pattern.
The Problem: Infinite Billing Rules
In my billing-service, I had to calculate how much a patient owes versus how much the insurance provider covers. But every patient is different:
- State Insurance (SGK): Covers 80% of the bill.
- Private Insurance: Might have a different percentage or specific discount.
- No Insurance: The patient pays 100%.
In a naive implementation, I would have one giant method with a switch statement for every insurance type. Every time I added a new provider, such as AXA or Mapfre, I would have to modify that same method, risking bugs in existing calculations.
The Solution: The Strategy Pattern
The Strategy Pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. It lets the algorithm vary independently from the clients that use it.
First, I defined a common interface for all insurance strategies:
package com.project.billing_service.strategy;
import java.math.BigDecimal;
public interface InsuranceStrategy {
InsuranceCalculationResult calculate(BigDecimal amount);
}
Then, I created concrete implementations for the specific rules. Here is how the state insurance (SGK) is calculated:
package com.project.billing_service.strategy;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
@Component
public class SgkStrategy implements InsuranceStrategy {
@Override
public InsuranceCalculationResult calculate(BigDecimal amount) {
BigDecimal insuranceOwes = amount.multiply(BigDecimal.valueOf(0.80)).setScale(2, RoundingMode.HALF_UP);
BigDecimal patientOwes = amount.subtract(insuranceOwes).setScale(2, RoundingMode.HALF_UP);
return new InsuranceCalculationResult(patientOwes, insuranceOwes);
}
}
Finally, I used a Factory class to determine which strategy to inject based on the provider's type or name at runtime:
package com.project.billing_service.strategy;
import org.springframework.stereotype.Component;
@Component
public class InsuranceFactory {
private final SgkStrategy sgkStrategy;
private final AllianzStrategy allianzStrategy;
private final NoInsuranceStrategy noInsuranceStrategy;
public InsuranceFactory(SgkStrategy sgkStrategy, AllianzStrategy allianzStrategy, NoInsuranceStrategy noInsuranceStrategy) {
this.sgkStrategy = sgkStrategy;
this.allianzStrategy = allianzStrategy;
this.noInsuranceStrategy = noInsuranceStrategy;
}
public InsuranceStrategy getStrategy(String providerType, String providerName) {
if (providerType != null && providerType.equalsIgnoreCase("NONE")) {
return noInsuranceStrategy;
}
if (providerType != null && providerType.equalsIgnoreCase("SGK")) {
return sgkStrategy;
}
if (providerName != null && providerName.equalsIgnoreCase("allianz")) {
return allianzStrategy;
}
if (providerType != null && providerType.equalsIgnoreCase("PRIVATE")) {
return noInsuranceStrategy;
}
return noInsuranceStrategy;
}
}
The Trade-offs
Class Explosion
You end up with many small classes instead of one big method. While this is cleaner, it can feel like "too much" for very simple systems.
Overhead
You need a factory to manage these strategies, adding one more layer of abstraction.
The Strategy Pattern turned a potentially messy piece of business logic into a modular, testable, and scalable component. It is a classic example of how a simple design pattern can save you from a future of debugging an "if-else" mess.