# Domain classes & the `@Ekbatan*` annotations Four annotations and five domain classes — that's everything you write to wire up Ekbatan in a DI-managed app. The annotations are pure markers; your classes don't depend on Spring, Quarkus, or Micronaut. The same source compiles and runs identically against all three. What each DI container does *under the hood* to find these annotated classes — Spring's auto-config + AOT processor, Quarkus' Jandex build steps, Micronaut's compile-time visitor — lives on the per-framework pages: [spring.md](spring.md), [quarkus.md](quarkus.md), [micronaut.md](micronaut.md). If you don't use a DI container at all, the same domain classes work without the annotations — see [wiring/without-di.md](without-di.md). ## The four annotations | Annotation | Apply to | What it tells the DI integration | |---|---|---| | **`@EkbatanAction`** | An `Action` subclass | Discover this class, register it as a framework-private singleton, and add it to `ActionRegistry` so `ActionExecutor.execute(...)` can find it. | | **`@EkbatanRepository`** | An `AbstractRepository<…>` subclass | Discover and register as a managed DI bean; inject into `RepositoryRegistry` keyed by domain class. | | **`@EkbatanEventHandler`** | An `EventHandler` implementation | Register as a managed DI bean; add to `EventHandlerRegistry` (only effective when the local-event-handler module is on the classpath). | | **`@EkbatanDistributedJob`** | A `DistributedJob` subclass | Register as a managed DI bean; add to `JobRegistry` (only effective when the distributed-jobs module is on the classpath). | All four are pure markers (`@Target(TYPE) @Retention(RUNTIME)`, no parameters) defined in `io.ekbatan.di:annotations`. They carry no behavior themselves; they exist so each DI integration can find your classes without classpath scanning every type. > `@AutoBuilder` (used on `Model`/`Entity` subclasses) is *not* one of the `@Ekbatan*` DI annotations — it's a compile-time builder generator, completely independent of DI. See [Models and Entities → @AutoBuilder](../concepts/models-and-entities.md#autobuilder--generated-builders). ## The five domain classes The running example: a `Wallet` model that supports deposits and closes, an action that performs deposits, a repository, an in-process event handler that reacts to deposits, and a daily report job. Five classes, five annotations (`@AutoBuilder` + four `@Ekbatan*`). ### Wallet — the Model ```java @AutoBuilder public final class Wallet extends Model, WalletState> { public final UUID ownerId; public final Currency currency; public final BigDecimal balance; Wallet(WalletBuilder builder) { super(builder); this.ownerId = Validate.notNull(builder.ownerId, "ownerId cannot be null"); this.currency = Validate.notNull(builder.currency, "currency cannot be null"); this.balance = Validate.notNull(builder.balance, "balance cannot be null"); } public static WalletBuilder createWallet(UUID ownerId, Currency currency, BigDecimal balance, Instant createdDate) { final var id = Id.random(Wallet.class); return WalletBuilder.wallet() .id(id) .state(OPENED) .ownerId(ownerId) .currency(currency) .balance(balance) .createdDate(createdDate) .withInitialVersion() .withEvent(new WalletCreatedEvent(id, ownerId, currency, balance)); } @Override public WalletBuilder copy() { return WalletBuilder.wallet() .copyBase(this) .ownerId(ownerId) .currency(currency) .balance(balance); } public Wallet deposit(BigDecimal amount) { Validate.notNull(amount, "amount cannot be null"); Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0, "Deposit amount must be positive"); final var newBalance = balance.add(amount); return copy() .withEvent(new WalletMoneyDepositedEvent(id, amount, newBalance)) .balance(newBalance) .build(); } public Wallet close() { if (state.equals(CLOSED)) { return this; } return copy() .withEvent(new WalletClosedEvent(id)) .state(CLOSED) .build(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; Wallet wallet = (Wallet) o; return ownerId.equals(wallet.ownerId) && currency.equals(wallet.currency) && balance.compareTo(wallet.balance) == 0; } } ``` ### WalletDepositAction — the Action `@EkbatanAction` registers it with `ActionRegistry`. Constructor params are resolved by the DI container. This annotation is discovery-only; execution behavior such as retries and cross-shard allowance is configured with `ExecutionConfiguration` on the `ActionExecutor` default or on a single `execute(...)` call. ```java @EkbatanAction public class WalletDepositAction extends Action { public record Params(Id walletId, BigDecimal amount) {} private final WalletRepository walletRepository; public WalletDepositAction(Clock clock, WalletRepository walletRepository) { super(clock); this.walletRepository = walletRepository; } @Override protected Wallet perform(Principal principal, Params params) { var wallet = walletRepository.getById(params.walletId().getValue()); var updated = wallet.deposit(params.amount()); return plan().update(updated); } } ``` ### WalletRepository — the Repository `@EkbatanRepository` registers it as a managed bean and into `RepositoryRegistry`. ```java @EkbatanRepository public class WalletRepository extends ModelRepository { public WalletRepository(DatabaseRegistry databaseRegistry) { super(Wallet.class, WALLETS, WALLETS.ID, databaseRegistry); } @Override public Wallet fromRecord(WalletsRecord r) { return WalletBuilder.wallet() .id(Id.of(Wallet.class, r.getId())) .version(r.getVersion()) .state(WalletState.valueOf(r.getState())) .ownerId(r.getOwnerId()) .currency(Currency.getInstance(r.getCurrency())) .balance(r.getBalance()) .createdDate(r.getCreatedDate()) .updatedDate(r.getUpdatedDate()) .build(); } @Override public WalletsRecord toRecord(Wallet w) { return new WalletsRecord( w.id.getValue(), w.version, w.state.name(), w.ownerId, w.currency.getCurrencyCode(), w.balance, w.createdDate, w.updatedDate); } } ``` ### WalletMoneyDepositedEventHandler — the EventHandler `@EkbatanEventHandler` registers it with `EventHandlerRegistry`. Only effective when the local-event-handler module is on the classpath. ```java @EkbatanEventHandler public class WalletMoneyDepositedEventHandler implements EventHandler { private final NotificationService notificationService; public WalletMoneyDepositedEventHandler(NotificationService notificationService) { this.notificationService = notificationService; } @Override public String name() { return "wallet-deposit-notification"; } @Override public Class eventType() { return WalletMoneyDepositedEvent.class; } @Override public void handle(EventEnvelope envelope) { notificationService.notifyDeposit(envelope.event.modelId, envelope.event.amount); } } ``` ### DailyWalletReportJob — the DistributedJob `@EkbatanDistributedJob` registers it with `JobRegistry`. Only effective when the distributed-jobs module is on the classpath. ```java @EkbatanDistributedJob public class DailyWalletReportJob extends DistributedJob { private final ReportService reportService; public DailyWalletReportJob(ReportService reportService) { this.reportService = reportService; } @Override public String name() { return "daily-wallet-report"; } @Override public Schedule schedule() { return Schedules.daily(LocalTime.of(2, 0)); } @Override public void execute(ExecutionContext ctx) { reportService.generateAndSend(); } } ``` ## What gets exposed as injectable beans Once these five classes are on your classpath and your DI container is wired up (per [spring.md](spring.md), [quarkus.md](quarkus.md), or [micronaut.md](micronaut.md)), the following beans are available for `@Inject` / `@Autowired` anywhere in your application: | Bean | Always available | Notes | |---|---|---| | `ActionExecutor` | yes | The thing application code calls | | `DatabaseRegistry` | yes | Direct shard / DSLContext access if you need it | | `RepositoryRegistry` | yes | Cross-type repository lookup | | `Clock` | yes | Defaults to `Clock.systemUTC()`, overridable in tests | | Each `@EkbatanRepository` instance | yes | Inject by its concrete class | | `EventHandlerRegistry` | only with local-event-handler on classpath | | | `EventFanoutJob` | only with local-event-handler on classpath | Auto-registered into `JobRegistry` | | `EventHandlingJob` | only with `ekbatan.local-event-handler.handling.enabled=true` | Opt-in (off by default) | | `JobRegistry` | only with distributed-jobs on classpath | | | `EventPersister` | not auto-registered — define your own bean to override the executor's default `SingleTableJsonEventPersister` (e.g. for encrypted payloads or an alternate sink) | | The `Action` instances are intentionally **not** exposed as DI beans — see the per-framework pages for the rationale. ## See also - [Wiring with Spring Boot](spring.md) — how Spring discovers and wires these classes (auto-config, AOT, native-image) - [Wiring with Quarkus](quarkus.md) — Quarkus-specific (Jandex, build steps, classloader safety, HikariCP RMR) - [Wiring with Micronaut](micronaut.md) — Micronaut-specific (`EkbatanStereotypeVisitor`, transitive-jar processing) - [Wiring without DI](without-di.md) — the same domain classes wired manually, no annotations needed - [Models and Entities](../concepts/models-and-entities.md) — the framework concepts behind `@AutoBuilder`, mutations, events - [Actions, ActionPlan, ActionExecutor](../concepts/actions.md) — what `@EkbatanAction` classes do - [Repositories on JOOQ](../database/repositories.md) — what `@EkbatanRepository` classes do - [Listen-to-yourself: in-process event handlers](../events/local-event-handler.md) — what `@EkbatanEventHandler` consumes - [Distributed background jobs](../jobs/distributed-jobs.md) — what `@EkbatanDistributedJob` schedules