How to Seamlessly Refactor MVC Projects into DDD Architecture
This article explains why legacy MVC codebases become tangled, introduces the domain‑driven design (DDD) layered architecture, and provides a step‑by‑step, low‑cost migration path—including layer mapping, call‑chain illustration, common pitfalls, and a concrete refactoring example with design patterns and code snippets.
Problem Overview
Legacy MVC projects suffer from severe architectural decay because objects, services, and components are cross‑used without clear boundaries, making long‑term iteration costly.
Why MVC Decays
MVC separates state (PO/VO/enum) from behavior (service), which speeds early delivery but over time leads to services calling each other and sharing domain objects, creating a tangled “big wardrobe” of code.
DDD Layered Architecture
Domain‑Driven Design (DDD) treats each bounded context as an independent “person” that owns its models, services, repositories, etc., resulting in clearer boundaries and lower maintenance cost.
A typical four‑layer DDD structure includes:
app : application startup, AOP, configuration, image building.
api : external RPC interface definitions.
domain : core business logic, aggregates, entities, value objects, repositories, events, services.
infrastructure : persistence, Redis, config center, event messaging, external service adapters.
gateway : external HTTP/RPC call encapsulation.
types : common DTOs, enums, response objects.
Migration from MVC to DDD
Key steps:
Move domain‑specific logic from service to the domain layer, creating dedicated sub‑packages (e.g., xxx, yyy, zzz).
Shift generic utilities such as Redis and configuration from service to the dao / infrastructure layer, applying dependency inversion.
Treat the original service as the application/case layer that orchestrates domain services.
Keep the export (RPC) layer unchanged unless the domain package is exposed, in which case move it to the export layer.
The mapping diagram shows MVC layers on the left and DDD layers on the right, using the same colour coding to illustrate correspondence.
Layered Call Chain
The DDD call chain from an interface implementation to each module is visualised, helping developers decide where to place specific functionality.
Common Pitfalls
Simply switching the skeleton from MVC to DDD does not automatically produce clean code; existing “old furniture” must be refactored, and appropriate design patterns should be applied.
Refactoring Example with Design Patterns
A credit‑limit‑adjustment scenario demonstrates the use of an abstract class as a template, strategy and factory patterns for rule validation, and dependency inversion for infrastructure services.
public AdjustAssetOrderEntity acceptAdjustAssetApply(AdjustAssetApplyEntity adjustAssetApplyEntity) {
// 1. 参数校验
this.parameterVerification(adjustAssetApplyEntity);
// 2. 查询申请单数据,如已经存在则直接返回
AdjustAssetOrderEntity orderEntity = queryAssetLog(adjustAssetApplyEntity.getPin(), adjustAssetApplyEntity.getAccountType(), adjustAssetApplyEntity.getTaskNo(), adjustAssetApplyEntity.getAdjustType());
if (null != orderEntity) {
log.info("pin={} taskNo={} 受理申请,检索到任务存在进行中的申请单。", adjustAssetApplyEntity.getUserId(), adjustAssetApplyEntity.getTaskNo());
return orderEntity;
}
// 3. 以下流程放到分布式锁内处理【避免相同请求二次进入】
String lockId = genLockId(adjustAssetApplyEntity.getAdjustType(), adjustAssetApplyEntity.getUserId());
try {
// 3.1 分布式锁:加锁
long state = lock(lockId);
if (0 == state) {
throw new AccountRuntimeException(BizResultCodeEm.DISTRIBUTED_LOCK_EXCEPTION.getCode(), "分布式锁异常,当前用户行为处理中。");
}
// 3.2 账户查询
UserAccountInfoDTO userAccountInfoDTO = queryJtAccount(adjustAssetApplyEntity.getUserId(), adjustAssetApplyEntity.getAccountType());
// 3.3 基础校验;(1)账户类型、(2)状态状态、(3)额度类型、(4)账户逾期、(5)费率类型【暂无】
LogicCheckResultEntity logicCheckResultEntity = doCheckLogic(adjustAssetApplyEntity, userAccountInfoDTO,
DefaultLogicFactory.LogicModel.ACCOUNT_TYPE_FILTER.getCode(),
DefaultLogicFactory.LogicModel.ACCOUNT_STATUS_FILTER.getCode(),
DefaultLogicFactory.LogicModel.ACCOUNT_QUOTA_FILTER.getCode(),
DefaultLogicFactory.LogicModel.ACCOUNT_OVERDUE_FILTER.getCode()
);
if (!AssetCycleQuotaAlterCodeEnum.E0000.getCode().equals(logicCheckResultEntity.getCode())) {
log.info("userId={} taskNo={} 规则校验过滤拦截。code:{} info:{}", adjustAssetApplyEntity.getUserId(), adjustAssetApplyEntity.getTaskNo(), logicCheckResultEntity.getCode(), logicCheckResultEntity.getInfo());
throw new AccountRuntimeException(logicCheckResultEntity.getCode(), logicCheckResultEntity.getInfo());
}
// 3.4 受理调额
return this.acceptAsset(adjustAssetApplyEntity, userAccountInfoDTO);
} finally {
// 3.1 分布式锁:解锁
this.unlock(lockId);
}
}The refactored code is clearer: it validates parameters, checks existing orders, acquires a distributed lock, performs business rule checks via a strategy/factory, and finally processes the adjustment.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
dbaplus Community
Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.
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.
