AI Guide (copyable)
**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
Request → Controller (adapter) → AppService (application) → BusFacade (CQRS bus)
↓
QueryHandler / CommandHandler
↓
Repository → DB / SLS| Layer | Responsibility | Depends on |
|---|---|---|
adapter/ | accept HTTP requests, validate, return VOs | → Application |
application/ | orchestrate business flows, dispatch via BusFacade | → Domain + Infrastructure |
domain/ | core business logic: aggregates + entities + events | depends on nothing |
infrastructure/ | persistence, external APIs, object mapping | → Domain (interfaces) |
Core classes
| Class | Role |
|---|---|
BusFacade | the 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 |
AbstractAggregateRoot | aggregate-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 |
BaseException | unified exception (RuntimeException; the throws clause is documentation) |
ParamChecker | argument validation; failure raises BaseException(BAD_REQUEST) |
File naming
| Layer | File | Notes |
|---|---|---|
| Domain outer | XxxData.java | SLS data envelope (ts/source/endpoint/data) |
| Domain inner | XxxDetail.java | SLS business fields |
| Domain aggregate root | {Name}.java | extends Entity |
| Domain entity | {Name}Entity.java | extends AbstractAggregateRoot |
| Query | ListXxxQuery.java / GetXxxQuery.java | read parameters |
| Command | CreatXxxCommand.java | write parameters (note the spelling is Creat, not Create — keep it consistent with the template) |
| VO | XxxVO.java | API response |
| DO | XxxDo.java | MySQL persistence |
| Repository | XxxReadRepository.java / XxxWriteRepository.java | interfaces in domain, implementations in infrastructure |
| Assembler | XxxAssembler.java | static toVo() / toDo() / toPageVo() |
| Handler | ListXxxQueryHandler.java / CreatXxxCommandHandler.java | implement the matching generic interface |
| Service | XxxAppService.java + XxxAppServiceImpl.java | interface + @Service |
| Controller | XxxController.java | @RestController @RequestMapping("/xxx") |
Two hard constraints
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
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.
// ✅ 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:
CommandHandlerreturns an aggregate root /DomainEvents/DomainEvent—CqrsAroundHandlerpublishes.Repository'ssave/update/delete/operateacting on anAbstractAggregateRoot—RepositoryEventAspectpublishes after the transaction commits.
Mutate the aggregate, return void, and skip *Repository persistence → no event. Aggregates must be persisted through the *Repository methods.
Configuration
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: 16The async thread-pool prefix is scoda.ddd.cqrs.async.*, not soda.*. Get the prefix wrong and it silently does nothing.
Build and test
mvn clean verify # build + test (what releases use)
mvn test # tests only
mvn clean install # install to the local repoJava 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
These are the ones AI steps in most often. Scan this list before writing code.
- Handlers are matched by exact class.
SimpleCommandBus/SimpleQueryBuslook uphandlers.get(command.getClass())— no inheritance. A handler written for a parent class will not catch a subclass command. Each command type may have exactly oneCommandHandler; a later registration overwrites the earlier one. - **
EventHandlerdoes walk the hierarchy.**SimpleEventBus.publishlooks up parent classes and interfaces, soEventHandler<DomainEvent>catches everything. sendMqCommandis a stub that always throwsUnsupportedOperationException. Never call it.AbstractAggregateRoot extends AbstractDomainEvent— the aggregate root is itself an event and carrieseventId/requestIdfields. Historical design; nothing to assign, leave it alone.@AutoConfigureAftertakes string class names, notClassreferences (for Spring Boot 3/4 compatibility). Copy that style when adding auto-configuration.PreCheckeris deprecated; useParamChecker.EventSerializeris what keeps Jackson 2/3 compatible. If you add Jackson annotations, use the Jackson 2 coordinatescom.fasterxml.jackson.annotation.*, not the Jackson 3 ones (those break on Spring Boot 4).knife4j-openapi3-spring-boot-starteris optional. When subclassingBaseCommand/BaseQuery/AbstractDomainEvent, the project itself must bringio.swagger.core.v3:swagger-annotationsor it will not compile.
Things you must not do
- Do not invent
@Command/@Query/@Handlerannotations — soda has no custom annotations at all. Everything isCommandHandler/QueryHandler/EventHandlerinterfaces 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.