Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Refactoring a Legacy System with 810K Lines of Java Code: A Case Study on Youku CRP's Payment Module

Tech 1

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

  1. Bloated, Rigid Code
    The submitPayment method (600+ lines) was a "procedural mess"—hard to debug and extend. Adding OTT payment support would’ve required duplicating code or nested if-else logic.

    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;
    }
    
  2. 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.

  3. 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-else checks.

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:

  1. Rule Validation: Use a "Loss Prevention Platform" to monitor payment amounts/status via APIs, SQL, or binlog changes.
  2. Smoke Tests: For HSF (RPC) services, validate downstream dependencies.
  3. 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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.