Refactoring a Multi‑Tenant Backend Service Using DDD and Factory Pattern
This article presents a case study of optimizing a multi‑tenant Java Spring MVC backend by applying domain‑driven design principles, the dependency inversion principle, and a factory pattern to decouple tenant‑specific logic, reduce service size, and improve maintainability and testability.
1. Background
In a multi‑tenant project that shares a single backend service built with MVC architecture, the code became tightly coupled across tenants, leading to high maintenance cost, bulky services (over 1000 lines), and difficulty in testing and extending functionality.
Tenant‑specific business scenarios are handled with if‑else statements.
Any change for one tenant requires testing all tenants that share the same code.
The service class grew to more than a thousand lines, containing logic for all tenants.
2. Redesign
Following Domain‑Driven Design (DDD), business rules are placed in the domain layer rather than the application layer, allowing each tenant’s logic to be encapsulated within its own domain module, achieving isolation.
Applying the Dependency Inversion Principle, abstractions are defined for common tenant behavior, while concrete implementations reside in tenant‑specific classes.
A factory pattern is introduced to obtain the appropriate tenant object at runtime and delegate business processing accordingly.
3. Refactored Code Example
Controller
@PostMapping("/infoByNamePage")
@ApiOperation(value = "分页查询", notes = "分页查询")
public R
> infoByNamePage(@Valid @RequestBody OrderInfoVoRequest orderInfoVoRequest) {
OrderAction orderAction = OrderFactory.getOrderAction(CurrentUserUtil.getTenantId());
return orderAction.infoByNamePage(orderInfoVoRequest);
}Here OrderAction is the top‑level interface that defines all tenant business behaviors.
OrderFactory Implementation
public class OrderFactory implements ApplicationContextAware {
// Tenant A
private static final String ORDER_TENANT_A = "A";
// Tenant B
private static final String ORDER_TENANT_B = "B";
@Autowired
private OrderTenantConfig config;
private static Map
orderActionMap = new HashMap<>(2);
/**
* Get the corresponding tenant implementation based on tenantId
*/
public static OrderAction getOrderAction(String tenantId) {
OrderAction orderAction = orderActionMap.get(tenantId);
if (ObjectUtil.isEmpty(orderAction)) {
throw new OrderBusinessException(OrderCodeEnum.TENANT_ID_IS_NULL);
}
return orderAction;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// Register AOrderAction and BOrderAction implementations
orderActionMap.put(config.getMap().get(ORDER_TENANT_A), applicationContext.getBean(AOrderAction.class));
orderActionMap.put(config.getMap().get(ORDER_TENANT_B), applicationContext.getBean(BOrderAction.class));
}
}After refactoring, the original 1000‑line service is split into three classes: AbstractOrderAction , AOrderAction , and BOrderAction . The abstract class still holds shared logic (about 500 lines), while each tenant implementation contains its specific behavior, greatly improving decoupling and maintainability.
4. Thoughts and Summary
This design is not suitable for extremely complex or numerous business scenarios, because the top‑level OrderAction interface may become overloaded, leading the abstract class to bloat and re‑creating a large service that needs further splitting.
In conclusion, choose the design that matches the scale of the project; there is no universally best solution, only the most appropriate one for the given context.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.