Refactoring a Legacy System with 810K Lines of Java Code: A Case Study on Youku CRP's Payment Module
Youku's CRP (Content Rights Procurement) system, a decade-old legacy application, struggles with outdated technical frameworks and fragmented code—810,000 lines of Java and 170,000 lines of JSP. When taking over the financial module, the goal was to modernize it while ensuring business continuity. The key principle: refactor incrementally alongside business needs, with a clear long-term vision for the system's architecture.
The 810K-Line Dilemma
Most of these lines are obsolete: unused services, migrated data with stale dependencies, and redundant jobs. Blindly auditing every service is cost-prohibitive. Instead, refactor on-demand—migrate code only when necessary, ensuring downstream dependencies remain unaffected.
The Payment Module: A Case Study
The payment module (≈30K lines) was prioritized for two reasons: high business urgency and clear code refactoring challenges. The goal? Preserve existing interfaces while migrating legacy code to a new project (driven by ROI, e.g., moving from JSP to a front-end/back-end separated architecture).
Business Goals for Payment
Payment must achieve:
- Zero Financial Loss: Accurate info validation, risk checks, precise amount calculation, and state consistency.
- Process Efficiency: Automated accounting, flexible payment methods, and streamlined approvals.
Technical Pain Points
-
Bloated, Rigid Code
ThesubmitPaymentmethod (600+ lines) was a "procedural mess"—hard to debug and extend. Adding OTT payment support would’ve required duplicating code or nestedif-elselogic.Original (simplified) anti-pattern:
@Transactional public Payment submitPayment(PaymentDto dto, User user) { // 600+ lines: initialization, DB calls, validation, updates, approval... paymentDao.insert(dto); // 50+ lines of validation (mixed with initialization) paymentDao.update(payment); // redundant update paymentDao.update(payment); // another redundant update // Async approval (mixed with business logic) return payment; } -
Scattered Logic, Low Reusability
Entity state changes (e.g., payment status) were spread across services, leading to bugs. For example, a payment status update was duplicated in two mappers, causing inconsistent states. -
Outdated Workflow Framework
Using Activiti 5 (2010 vintage), workflow logic was tightly coupled with business code. A single service had 4,000+ lines and 600+if-elsechecks.
Solutions
1. Top-Down Process Decomposition
Split the monolithic submitPayment into layered commands and phases (Command-Phase pattern):
Refactored Structure:
// Command: Orchestrate phases
@Service
public class PaymentSubmitCmdExecutor {
@Autowired private InitPhase initPhase;
@Autowired private ValidationPhase validationPhase;
@Autowired private ProcessingPhase processingPhase;
@Transactional
public Payment execute(PaymentRequest request) {
var context = initPhase.init(request);
validationPhase.validate(context);
processingPhase.process(context);
return context.getPayment();
}
}
// Phase: Single responsibility (e.g., validation)
public class ValidationPhase {
public void validate(PaymentContext context) {
validateDuplicateSubmission(context);
validateBaseInfo(context);
validateBills(context);
// Extensible: override for OTT
}
}
Extending for OTT Payments:
// OTT-specific command (extends base)
@Service
public class OttPaymentSubmitCmdExecutor extends PaymentSubmitCmdExecutor {
@Autowired private OttProcessingPhase ottPhase;
// Override phase for OTT logic
}
// OTT-specific processing
@Service
public class OttProcessingPhase extends ProcessingPhase {
@Override
public void process(PaymentContext context) {
super.process(context); // Reuse base logic
startOttApproval(context); // OTT-specific approval
}
}
2. Architecture Isolation & Logic Convergence
Adopt Domain-Driven Design (DDD) principles:
- Domain Layer: Encapsulate business logic (e.g., payment status changes) in domain services.
- Hexagonal Architecture: Domain depends on abstractions (gateways), not direct DAO calls.
Refactored Domain Service:
// Domain service (encapsulates state changes)
@Service
public class PaymentDomainService {
@Autowired private PaymentRepository repo;
public void confirmPayment(Payment payment) {
payment.validateConfirmation(); // Domain logic
repo.updateStatus(payment, PaymentStatus.CONFIRMED); // Single source of truth
}
}
// App Layer (orchestrates domain)
@Component
public class ConfirmPaymentCmdExecutor {
@Autowired private PaymentDomainService domainService;
public void execute(ConfirmRequest request) {
var payment = repo.getById(request.getId());
domainService.confirmPayment(payment);
}
}
3. Modernize Workflow with Design Patterns
Migrate from Activiti to the company’s BPMS, using Strategy + Factory to replace if-else chaos:
Strategy Pattern (Approval Logic):
// Abstract strategy
public abstract class ApprovalStrategy {
public abstract void execute(Payment payment);
}
// Concrete strategy (Finance Approval)
@Service("financeApproval")
public class FinanceApprovalStrategy extends ApprovalStrategy {
@Override
public void execute(Payment payment) { /* Finance logic */ }
}
// Factory to resolve strategies
@Service
public class ApprovalFactory {
private final Map<String, ApprovalStrategy> strategies;
@Autowired
public ApprovalFactory(List<ApprovalStrategy> strategies) {
this.strategies = strategies.stream()
.collect(Collectors.toMap(
s -> s.getClass().getAnnotation(Service.class).value(),
Function.identity()
));
}
public void execute(String strategyKey, Payment payment) {
strategies.get(strategyKey).execute(payment);
}
}
Ensuring Quality
To prevent financial loss:
- Rule Validation: Use a "Loss Prevention Platform" to monitor payment amounts/status via APIs, SQL, or binlog changes.
- Smoke Tests: For HSF (RPC) services, validate downstream dependencies.
- Unit Tests: Focus on core domain logic (now modularized). Mocking is simpler with layered architecture.
Conclusion
Refactoring legacy systems requires:
- Business-Centric Prioritization: Address pain points (e.g., bloated code → process decomposition; scattered logic → DDD).
- Incremental Change: Migrate/rewrite only when ROI justifies it.
- Design Patterns + Architecture: Balance reusability and maintainability.
The payment module’s refactoring (from 600+ line monoliths to 200-line entry points) demonstrates how to tame technical debt without risking business disruption.