How to Refactor an Order Service for Better Maintainability and Testability

This article walks through a simple e‑commerce order flow, highlights the drawbacks of tightly coupled service code, and demonstrates step‑by‑step refactorings—introducing repository abstractions, domain entities, an anti‑corruption layer, and a generic message producer—to achieve a clean, extensible, and testable backend architecture.

ITFLY8 Architecture Home
ITFLY8 Architecture Home
ITFLY8 Architecture Home
How to Refactor an Order Service for Better Maintainability and Testability

Explanation

This simple example uses an e‑commerce order scenario to illustrate how to write good business code.

The case is a simplified simulation and may differ from real‑world situations.

Scenario: a user selects a product, adds it to the cart, and then places an order.

Select product

Place order

Checkout

Generate order

Notify

The business logic for submitting an order is as follows:

Validate account legality

Call third‑party API to get discounted price

Deduct wallet amount

Generate order information

Notify user of successful order

Code Implementation

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private KafkaTemplate kafkaTemplate;

    /**
     * Purchase product and submit order
     * @param userId   User ID
     * @param productId Product ID
     */
    public Result submit(Long userId, Long productId) throws BizException {
        // Validate account
        UserDO userDO = userMapper.findById(userId);
        if (userDO == null) {
            throw BizException(USER_NOT_EXISTS);
        }
        // Get product and discount info
        ProductDO productDO = productMapper.findById(productId);
        Double delta = HttpUtils.getDiscount(productId);
        double actualPayment = productDO.getPrice() - delta;
        Money money = userDO.getMoney();
        if (actualPayment > money.getRemain()) {
            // Insufficient balance
            return Result.fail("余额不足");
        }
        // Deduct amount
        double remain = money.getRemain() - actualPayment;
        money.setRemain(remain);
        // Update wallet balance
        userMapper.update(userDO);
        // Generate order
        OrderDO orderDO = new OrderDO();
        orderDO.setUserId(userId);
        orderDO.setProductId(productId);
        orderMapper.save(orderDO);
        // Notify user
        kafkaTemplate.send("orderTopic", orderDO);
        return Result.ok();
    }
}

Although the code works, it suffers from several issues as the business evolves:

Poor maintainability:

MyBatis mappers embed data‑access details in the service; changing the persistence technology forces changes in business code.

Data objects (DO) are tightly coupled to table structures; schema changes affect the service.

Direct calls to third‑party APIs (HttpUtils) make the service brittle when signatures change.

Direct use of KafkaTemplate ties the service to a specific MQ implementation.

Poor extensibility:

Additional discounts or promotional rules require frequent modifications.

Business logic is strongly dependent on the data storage structure.

Poor testability:

Direct dependencies on database, third‑party services, and middleware make isolated testing difficult.

Code Optimization 1

Introduce repository interfaces to isolate data access from business logic.

public interface UserRepository {
    User findById(Long userId);
}

Repositories return domain objects, not data objects, and decouple the service from a specific persistence mechanism.

Code Optimization 2

Encapsulate behavior inside domain entities to avoid anemic models.

public class Money {
    private double remain;

    public double getRemain() { return remain; }
    public void setRemain(double remain) { this.remain = remain; }

    /**
     * Charge the specified amount.
     */
    public boolean charge(double delta) {
        if (remain < delta) {
            return false;
        }
        this.remain -= delta;
        return true;
    }
}

Code Optimization 3

Add an anti‑corruption (adapter) layer for third‑party discount services.

/** Anti‑corruption layer */
@Service
public class PayServiceImpl implements PayService {
    @Autowired
    private DiscountFacade discountFacade;

    /**
     * Pay for a product using the user's money.
     */
    public boolean pay(Money money, Product product) {
        Double delta = discountFacade.getDiscount(product.getId());
        return money.charge(product.getPrice() - delta);
    }
}

Code Optimization 4

Abstract the message producer to avoid direct dependency on a concrete MQ implementation.

public interface MessageProducer<T, R> {
    Result<R> send(T message);
}

Summary

The optimized code combines the above improvements:

@Autowired
private UserRepository userRepository;
@Autowired
private ProductRepository productRepository;
@Autowired
private OrderRepository orderRepository;
@Autowired
private MessageProducer<Order, Result> messageProducer;
@Autowired
private PayService payService;

/**
 * Purchase product and submit order
 */
public Result submit(Long userId, Long productId) throws BizException {
    // Validate user
    User user = userRepository.findByUserId(userId);
    if (user == null) {
        throw BizException(USER_NOT_EXISTS);
    }
    // Pay
    Product product = productRepository.findById(productId);
    boolean success = payService.pay(user.getMoney(), product);
    if (!success) {
        return Result.fail("费用扣除失败");
    }
    // Update account
    userRepository.update(user);
    // Create order
    Order order = OrderFactory.create(user, product);
    orderRepository.add(order);
    // Notify user
    messageProducer.send(order);
    return Result.ok();
}

The key goals are:

Independence from any specific framework.

Independence from UI presentation.

Independence from underlying data sources (MySQL, Oracle, MongoDB, etc.).

Independence from external dependencies; core business logic remains stable.

Testability: business logic can be verified without real databases, UI, or external services.

Source: https://www.toutiao.com/i6903053083555807752/

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaCode RefactoringClean ArchitectureService Layer
ITFLY8 Architecture Home
Written by

ITFLY8 Architecture Home

ITFLY8 Architecture Home - focused on architecture knowledge sharing and exchange, covering project management and product design. Includes large-scale distributed website architecture (high performance, high availability, caching, message queues...), design patterns, architecture patterns, big data, project management (SCRUM, PMP, Prince2), product design, and more.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.