Skip to content

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 14:02
· 21 commits to main since this release

Reliability and the production path. Projections converge when refreshes overlap, @Idempotent releases only its own registration, idempotent event consumers have a stable identity, and the four-layer rule keeps application services off infrastructure. Background work carries its tenant: projection marks, integration events, @Async tasks. Projections and event publications get operations: failure lists, retries, metrics and scheduled maintenance. ddk-mall recovers lost payments and has a secure profile. Upgrading from 0.8 takes a few steps, all listed in the upgrade guide.

Breaking

  • LAYERED_ARCHITECTURE_RULE no longer lets application services reference infrastructure classes. The four-layer documentation always said that infrastructure only implements domain ports; the rule now agrees, as the three-layer rule already did. Depend on the domain port instead. Wiring that has to name an implementation goes into a @Configuration class outside the layers, which both layering rules now exempt
  • Projections is now an interface; the implementation moved to the starter's internal package. Its public constructor took internal types, so no application could call it without breaking the internal-package rule. Injecting Projections is unchanged, a test can stub it without a mocking library, and an application bean of the type replaces the starter's

Added

  • DdkContainers.elasticsearch() in ddk-test: a single-node Elasticsearch 9 without authentication, for integration tests
  • ddk-mall can keep its order search read model in Elasticsearch (search profile). The projection, the API and the MCP tool are unchanged; only the implementation of the OrderSearchIndex port is swapped
  • IntegrationEventConsumer.contract() and EventContract: a consumer declares the event type and versions it understands. Messages of another type are not given to it, and messages of another version are skipped with a warning, or rejected for redelivery, before their payload is decoded. Without a declaration nothing changes. ddk-mall's consumers declare their contracts
  • Projections in multi-tenant applications: a mark records the tenant of the current OperatorContext, and the worker refreshes it as the system operator ddk-projection in that tenant, also after a restart or on another instance. An ID that is only unique within a tenant no longer shares one mark across tenants. A mark without a tenant still fails against ddk-mybatis-starter's tenant interceptor and is retried, instead of reading every tenant's data
  • Tenants for integration events: @IntegrationEvent(tenant = "...") names the accessor whose value is sent in the ddk-event-tenant header, and a consumer whose runInEventTenant() returns true handles the event, including its inbox registration, as the system operator ddk-event in that tenant. A message without the header fails instead of running without a tenant. Consumers that do not opt in are unchanged
  • ddk-job-starter and ddk-mcp-starter document how their entry points get an operator and tenant: a scheduled job runs each tenant through OperatorContext.runAs as a system operator; an MCP tool on the default synchronous, streamable server sees the operator that the HTTP filter set, which a new test pins
  • Projection operations: Projections.failures(name, limit) lists marks that keep failing with their last error, and Projections.retryFailed(name) retries them at once. With Micrometer present the starter publishes ddk.projection.refresh (timer by outcome) and the ddk.projection.pending, ddk.projection.failing and ddk.projection.oldest.age gauges
  • Event maintenance (ddk.event.maintenance.enabled, off by default): on a schedule, resubmits failed publications through Spring Modulith, deletes old completed publications and purges old inbox records, under a ShedLock lock when the application has a LockProvider. With Micrometer present, ddk.event.publications{status} counts publications by status
  • ddk-mall recovers payments whose outcome was lost: the attempt is committed before the channel is called, settlement runs in its own transaction and refunds when the order was cancelled meanwhile, and ReconcilePaymentsJob queries the channel for payments that were charged but never settled. PaymentChannel gains query; the simulated channel keeps an in-memory ledger
  • ddk-mall has a secure profile: requests need a login, the customer comes from the authenticated user instead of the X-Customer-Id header, rebuilds, stock changes and actuator are for the support role, and an MCP tool's customerId argument must be the signed-in customer unless support is calling. The default profile stays an open development mode
  • CommonArchRules.domainMustNotDependOn(String...): the domain purity rule with packages of your own added, such as an internal SDK

Changed

  • The default scope of @Idempotent and @RateLimit is now the full class name, method name and parameter types (com.acme.OrderService#place(PlaceOrderCommand)) instead of OrderService.place, so classes with the same simple name and overloaded methods no longer share registrations or quotas. The Redis keys change accordingly: during a deployment, a request registered under the old key is not recognised as a duplicate by the new version for up to one TTL, and rate-limit windows start afresh. Set scope explicitly to keep a key stable
  • scripts/ddk.sh installs the latest release (vX.Y.Z tag) by default instead of main. Set DDK_REF=main for the development line
  • ddk-projection-starter changes its pending table: new columns tenant_id, generation, claim_token and claimed_until, and a primary key that includes the tenant. Because the primary key changes, a 0.8 table is upgraded by copying it into the new structure; existing marks are kept and count as having no tenant. With initialize-schema=true this happens at startup; with initialize-schema=false the application now fails at startup, printing the statements, when the table is missing or has the 0.8 structure. Upgrade with the 0.8 instances stopped, or rebuild the projection afterwards. An upgrade that stops halfway leaves <table>_upgrade behind, and the next start finishes it from there instead of leaving its marks unprocessed. Invalid settings (batch size below 1, non-positive durations, retry-delay above max-retry-delay) also fail at startup
  • ddk-projection-starter creates an index on available_at for the sweep that picks due marks. With initialize-schema=true it is added to existing tables at startup; with initialize-schema=false a missing index is logged with the statement to run
  • An idempotent integration event consumer that Spring proxies (for example because handle is @Transactional) is now recorded in the inbox under its own class name instead of the proxy's generated name. Events it handled before the upgrade and that are redelivered afterwards are handled again; override consumerId() with the old name (Name$$SpringCGLIB$$0) if that matters during the upgrade

Fixed

  • OperatorContext did not follow work onto other threads: @Async methods and tasks on Spring Boot's executors ran without an operator, so in a multi-tenant application their first statement failed. ddk-core now registers a Micrometer context-propagation accessor for it, so the ContextPropagatingTaskDecorator that ddk-tracer-starter registers carries the operator, tenant included, and restores the thread afterwards
  • @Idempotent released registrations by key only: a call that ran past the TTL and then failed deleted the registration of a newer call with the same key, letting a third one in. Each registration now stores its own token and is removed with a compare-and-delete. A failure to remove it no longer replaces the method's exception
  • Projections could keep a stale read-model entry with nothing left to correct it when two instances refreshed the same entry: a worker that read old state and finished last overwrote the newer state, and its acknowledgement removed nothing but also changed nothing. Marks also restarted at version 1 after deletion, so an old acknowledgement could delete a newer mark. Each mark now has a generation that is never reused and is claimed with a lease (ddk.projection.lease-time); a worker whose acknowledgement misses marks the entry again, so the read model always converges
  • Idempotent integration event consumers in one group were told apart by their simple class name. Two classes with the same simple name shared one inbox identity, so the second skipped every event the first had handled. IntegrationEventConsumer.consumerId() now names a consumer explicitly, and identities that clash within a group fail at startup. Consumers without a clash keep their identity, so existing inbox records stay valid
  • scripts/ddk.sh reused any earlier installation, so a changed DDK_REF or a newer commit on the same ref was never installed. It now records the ref, commit and version it installed in ~/.ddk/installed and reuses an installation only when all three still match and the archetypes are in the local repository
  • DOMAIN_MUST_NOT_DEPEND_ON_FRAMEWORKS did not cover Jackson 3 (tools.jackson..), the Jackson that Spring Boot 4 uses, so domain classes could depend on it unnoticed
  • The four-layer template advised AFTER_COMMIT for every domain event subscriber, including work that must happen with the business change. The ddk-add-domain-event skill now chooses by purpose: @EventListener inside the transaction for integration messages and read-model marks, AFTER_COMMIT only for notifications that may be lost, an integration event for work that must complete