Backend Development 21 min read

Applying Domain Events, Saga, and CQRS in Domain-Driven Design

The article, the second in a DDD practice series, shows how immutable domain events can capture business milestones and be stored and published, introduces Saga—both choreography and orchestration—to manage distributed transactions with compensations, and explains how CQRS separates read and write models, illustrating each concept with Java code while highlighting the resulting decoupling, scalability, and modeling clarity alongside added complexity and storage challenges.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
Applying Domain Events, Saga, and CQRS in Domain-Driven Design

This article is the second part of the "Domain‑Driven Design (DDD) Practice" series. It explains how to use domain events to separate core business complexity, introduces Saga for distributed transaction handling, and discusses CQRS as a complementary pattern.

Domain Events

Domain events are a DDD concept used to capture significant occurrences in the business domain. They become part of the ubiquitous language and serve as immutable milestones that can be traced and stored.

Typical characteristics of a domain event include high business value, contribution to a closed business loop, and clear boundaries. For example, in a cross‑border logistics scenario, "GoodsArrivedBondedWarehouse" is a domain event.

When modeling events, the name should reflect the command that triggered it, e.g., GoodsArrivedBondedWarehouseEvent or simply ArrivedBondedWarehouseEvent within a bounded context.

Below is a minimal Java definition of a domain event:

package domain.event;
import java.util.Date;
import java.util.UUID;
/**
 * @Description:
 * @Author: zhangwenbo
 * @Since: 2019/3/6
 */
public class DomainEvent {
    /**
     * 领域事件还包含了唯一ID,
     * 但是该ID并不是实体(Entity)层面的ID概念,
     * 而是主要用于事件追溯和日志。
     * 如果是数据库存储,该字段通常为唯一索引。
     */
    private final String id;

    /**
     * 创建时间用于追溯,另一方面不管使用了
     * 哪种事件存储都有可能遇到事件延迟,
     * 我们通过创建时间能够确保其发生顺序。
     */
    private final Date occurredOn;

    public DomainEvent() {
        this.id = String.valueOf(UUID.randomUUID());
        this.occurredOn = new Date();
    }
}

Domain events must be immutable and should carry only the context needed for the event, not the whole aggregate state. An example of a concrete event:

public class AddressUpdatedEvent extends DomainEvent {
    //通过userId+orderId来校验订单的合法性;
    private String userId; 
    private String orderId;
    //新的地址
    private Address address;
    //略去具体业务逻辑
}

Event Storage

Because events are immutable and need to be traceable, they must be persisted. Common storage options include a dedicated EventStore (e.g., MySQL, Redis, MongoDB) or co‑storing events with business data. When using a separate store, avoid distributed transactions; instead rely on eventual consistency and compensation.

# 考虑是否需要分表,事件存储建议逻辑简单
CREATE TABLE `event_store` (
  `event_id` int(11) NOT NULL auto increment,
  `event_code` varchar(32) NOT NULL,
  `event_name` varchar(64) NOT NULL,
  `event_body` varchar(4096) NOT NULL,
  `occurred_on` datetime NOT NULL,
  `business_code` varchar(128) NOT NULL,
  UNIQUE KEY (`event id`)
) ENGINE=InnoDB COMMENT '事件存储表';

Publishing Events

Events can be published directly by aggregates or via an event bus. The bus looks up handlers by the event’s class name and invokes those whose action matches. After handling, the event is saved to the store.

package domain;

import domain.event.DomainEvent;
import domain.handler.event.DomainEventHandler;
import java.util.List;

/**
 * @Description:
 * @Author: zhangwenbo
 * @Since: 2019/3/6
 */
public class DefaultDomainEventBus {

    public static void publish(DomainEvent event, String action,
                               EventCallback callback) {
        List
handlers = DomainRegistry.getInstance()
            .find(event.getClass().getName());
        handlers.stream().forEach(handler -> {
            if (action != null && action.equals(handler.getAction())) {
                Exception e = null;
                boolean result = true;
                try {
                    handler.handle(event);
                } catch (Exception ex) {
                    e = ex;
                    result = false;
                    //自定义异常处理
                    。。。
                } finally {
                    //write into event store
                    saveEvent(event);
                }
                if (callback != null) {
                    callback.callback(event, action, result, e);
                }
            }
        });
    }
}

Saga – Distributed Transaction Pattern

Saga breaks a global transaction into a series of local transactions, each with a compensating action. It avoids the need for two‑phase commit and works well with microservices.

Two implementation styles are described:

Choreography – each participant publishes events and reacts to others’ events.

Orchestration – a central saga orchestrator sends commands and receives callbacks.

Example of choreography steps (order creation flow) is illustrated in the article.

Compensation strategies are crucial. Transactions are classified as:

Compensatable – have a rollback action.

Critical – determine saga success.

Repeatable – do not need compensation.

CQRS

Command‑Query Responsibility Segregation separates read and write models. In DDD, CQRS works hand‑in‑hand with events and saga to reduce complexity. The article shows a read/write service layer that delegates to aggregates and publishes events.

Infrastructure Code Samples

Event registration via a singleton registry:

package domain;

import domain.event.DomainEvent;
import domain.handler.event.DomainEventHandler;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Description: 事件注册逻辑
 * @Author: zhangwenbo
 * @Since: 2019/3/6
 */
public class DomainRegistry {

    private Map
> handlerMap =
        new HashMap
>();
    private static DomainRegistry instance;

    private DomainRegistry() {}

    public static DomainRegistry getInstance() {
        if (instance == null) {
            instance = new DomainRegistry();
        }
        return instance;
    }

    public List
find(String name) {
        if (name == null) return null;
        return handlerMap.get(name);
    }

    public void register(Class
domainEvent,
                         DomainEventHandler handler) {
        if (domainEvent == null) return;
        if (handlerMap.get(domainEvent.getName()) == null) {
            handlerMap.put(domainEvent.getName(), new ArrayList
());
        }
        handlerMap.get(domainEvent.getName()).add(handler);
        //按照优先级进行事件处理器排序
        。。。
    }
}

Example of a concrete handler that registers itself and processes an event:

package domain.handler.event;

import domain.DomainRegistry;
import domain.StateDispatcher;
import domain.entity.meta.MetaActionEnums;
import domain.event.DomainEvent;
import domain.event.MetaEvent;
import domain.repository.meta.MetaRepository;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

/**
 * @Description:一个事件操作的处理器
 * @Author: zhangwenbo
 * @Since: 2019/3/6
 */
@Component
public class MetaConfirmUploadedHandler implements DomainEventHandler {

    @Resource
    private MetaRepository metaRepository;

    public void handle(DomainEvent event) {
        // 业务逻辑示例
        DomainEvent domainEvent = metaRepository.load();
        // ...
        domainEvent.setStatus(nextState);
        StateDispatcher.dispatch();
    }

    @PostConstruct
    public void autoRegister() {
        DomainRegistry.getInstance().register(MetaEvent.class, this);
    }

    public String getAction() {
        return MetaActionEnums.CONFIRM_UPLOADED.name();
    }

    public Integer getPriority() {
        return PriorityEnums.FIRST.getValue();
    }
}

Conclusion

The article emphasizes that domain events, Saga, and CQRS can be adopted partially; you do not need to implement the whole stack. Each technique brings benefits such as decoupling, scalability, and clearer business modeling, but also introduces complexity, learning curves, and challenges in event storage and querying.

JavaMicroservicesDomain-Driven DesignCQRSEvent SourcingDomain EventsSaga
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

0 followers
Reader feedback

How this landed with the community

login 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.