soda docs v17.0.1

AI Guide (copyable)

Important

**This page is that file.** Copy everything below and save it under the filename from the install table at the project root. Every "you" below refers to the AI assistant.


You are writing code for a Spring Boot project built on soda-core (com.hibuka.soda:soda-core-base). It is a DDD + CQRS framework. Follow the conventions below. Do not invent your own style.

Architecture

text
Request → Controller (adapter) → AppService (application) → BusFacade (CQRS bus)
                                                                  ↓
                                            QueryHandler / CommandHandler
                                                                  ↓
                                              Repository → DB / SLS
LayerResponsibilityDepends on
adapter/accept HTTP requests, validate, return VOs→ Application
application/orchestrate business flows, dispatch via BusFacade→ Domain + Infrastructure
domain/core business logic: aggregates + entities + eventsdepends on nothing
infrastructure/persistence, external APIs, object mapping→ Domain (interfaces)

Core classes

ClassRole
BusFacadethe only entry point. sendQuery(query) / sendTransactCommand(cmd) / sendAsyncCommand(cmd)
BasePageQuery<PageVO<XxxVO>>base class for every paged Query; carries pageNumber / pageSize (MAX_PAGE_SIZE=200)
QueryHandler<Q, R>implement R handle(Q), reads
CommandHandler<C, R>implement R handle(C), writes
EventHandler<T extends DomainEvent>implement void handle(T), domain events
AbstractAggregateRootaggregate-root base class; addPendingEvent(event) registers events
WriteRepository<T>four persistence methods: save / update / delete / operate
PageVO<T> / PageResult<T>repositories return PageVO, controllers return PageResult
Result<T>return wrapper for non-paged endpoints
BaseExceptionunified exception (RuntimeException; the throws clause is documentation)
ParamCheckerargument validation; failure raises BaseException(BAD_REQUEST)

File naming

LayerFileNotes
Domain outerXxxData.javaSLS data envelope (ts/source/endpoint/data)
Domain innerXxxDetail.javaSLS business fields
Domain aggregate root{Name}.javaextends Entity
Domain entity{Name}Entity.javaextends AbstractAggregateRoot
QueryListXxxQuery.java / GetXxxQuery.javaread parameters
CommandCreatXxxCommand.javawrite parameters (note the spelling is Creat, not Create — keep it consistent with the template)
VOXxxVO.javaAPI response
DOXxxDo.javaMySQL persistence
RepositoryXxxReadRepository.java / XxxWriteRepository.javainterfaces in domain, implementations in infrastructure
AssemblerXxxAssembler.javastatic toVo() / toDo() / toPageVo()
HandlerListXxxQueryHandler.java / CreatXxxCommandHandler.javaimplement the matching generic interface
ServiceXxxAppService.java + XxxAppServiceImpl.javainterface + @Service
ControllerXxxController.java@RestController @RequestMapping("/xxx")

Two hard constraints

Warning

Violating these either compiles fine and blows up at runtime, or silently drops domain events. Check both before writing code.

1. The generic must agree in three places

text
ListXxxQuery extends BasePageQuery<PageVO<XxxVO>>
  ⇒ QueryHandler<ListXxxQuery, PageVO<XxxVO>>
  ⇒ XxxAppService.listXxx(...) returns PageVO<XxxVO>

The R in all three must be the same type. One missing type argument, or writing XxxData where XxxVO belongs, breaks things.

2. CommandHandler must return the aggregate root

The return value of CommandHandler.handle(C) is what domain events ride on. Return void or a VO and CqrsAroundHandler has nothing to publish — the domain events are silently never sent.

java
// ✅ correct
public class CreatXxxCommandHandler implements CommandHandler<CreatXxxCommand, Xxx> {
    public Xxx handle(CreatXxxCommand cmd) {
        Xxx agg = Xxx.create(cmd);          // calls addPendingEvent(...) internally
        writeRepository.save(agg);           // RepositoryEventAspect publishes
        return agg;                          // ← must return
    }
}

// ❌ wrong: events never leave the process
public Void handle(CreatXxxCommand cmd) { writeRepository.save(...); return null; }

When domain events are published

Exactly two paths. Miss one and nothing is published:

  1. CommandHandler returns an aggregate root / DomainEvents / DomainEvent — CqrsAroundHandler publishes.
  2. Repository's save / update / delete / operate acting on an AbstractAggregateRoot — RepositoryEventAspect publishes after the transaction commits.

Mutate the aggregate, return void, and skip *Repository persistence → no event. Aggregates must be persisted through the *Repository methods.

Configuration

yaml
soda:
  event:
    bus-type: simple          # simple | redis, default simple
    redis:                    # only used when bus-type=redis
      stream:
        group-name: my-group
        consumer-name: my-consumer
        idempotency:
          enabled: true

# note the prefix is scoda., not soda.
scoda:
  ddd:
    cqrs:
      async:
        core-pool-size: 8
        max-pool-size: 16
Warning

The async thread-pool prefix is scoda.ddd.cqrs.async.*, not soda.*. Get the prefix wrong and it silently does nothing.

Build and test

bash
mvn clean verify          # build + test (what releases use)
mvn test                  # tests only
mvn clean install         # install to the local repo

Java 17+, 21 recommended. Do not use the cp pom-3.2.xml pom.xml multi-POM workflow — it is deprecated; the root pom-2.3.xml / pom-2.7.xml / pom-3.2.xml files are leftovers.

Common traps

Caution

These are the ones AI steps in most often. Scan this list before writing code.

  • Handlers are matched by exact class. SimpleCommandBus / SimpleQueryBus look up handlers.get(command.getClass()) — no inheritance. A handler written for a parent class will not catch a subclass command. Each command type may have exactly one CommandHandler; a later registration overwrites the earlier one.
  • **EventHandler does walk the hierarchy.** SimpleEventBus.publish looks up parent classes and interfaces, so EventHandler<DomainEvent> catches everything.
  • sendMqCommand is a stub that always throws UnsupportedOperationException. Never call it.
  • AbstractAggregateRoot extends AbstractDomainEvent — the aggregate root is itself an event and carries eventId / requestId fields. Historical design; nothing to assign, leave it alone.
  • @AutoConfigureAfter takes string class names, not Class references (for Spring Boot 3/4 compatibility). Copy that style when adding auto-configuration.
  • PreChecker is deprecated; use ParamChecker.
  • EventSerializer is what keeps Jackson 2/3 compatible. If you add Jackson annotations, use the Jackson 2 coordinates com.fasterxml.jackson.annotation.*, not the Jackson 3 ones (those break on Spring Boot 4).
  • knife4j-openapi3-spring-boot-starter is optional. When subclassing BaseCommand / BaseQuery / AbstractDomainEvent, the project itself must bring io.swagger.core.v3:swagger-annotations or it will not compile.

Things you must not do

  • Do not invent @Command / @Query / @Handler annotations — soda has no custom annotations at all. Everything is CommandHandler / QueryHandler / EventHandler interfaces plus @Component.
  • Do not inject a repository's implementation class into the application layer; inject only the interface (otherwise the application layer depends on the infrastructure layer).
  • Do not call handlers directly, bypassing BusFacade.
  • Do not put business rules in a Command — that is the aggregate root's job. Commands carry business parameters only; the framework fills in the context fields.