All notable changes to vicander (the DDD.* modules). The project follows
Semantic Versioning; until 1.0, a minor version may change the API.
0.1.1 - 2026-11-03
The first version on Hex. 0.1.0 was only a git tag, and its CI failed on a formatting check.
Added
- Your own Ecto schema.
use DDD.Repository.Ecto, ..., schema: MyApp.OrderRecordstores an aggregate in a table you already have (instead oftable:; both is an error). Fields map by name; overrideto_domain/1/from_domain/1(or callto_domain/2/from_domain/2with the fields that differ) for the rest. The save stays the framework's (yourchangeset/2is never called). The schema is checked when the adapter compiles (:version, primary key, JSON columns, unmapped fields).mix vic.gen.migrationskips such aggregates;DDD.Migration.schema_drift/2andDDD.Migration.table_drift/1report a mismatch.
Changed (breaking)
- The framework is a library. It starts nothing and reads nothing from your own
application's configuration. Configure
config :vicander, repo: MyApp.Repoand put{DDD.Supervisor, handlers: [...]}in your supervision tree.Vicander.RepoandVicander.Applicationare gone; the default repo of commands, queries, handlers, projections and the Mix tasks is the configured one. - Mix tasks are
mix vic.*(they weremix ddd.*):vic.gen.migration,vic.gen.outbox_migration,vic.outbox.dead_letters,vic.outbox.replay,vic.outbox.discard,vic.health,vic.projection.rebuild,vic.mcp. - Identity. Several fields can form an identity (
identity :tenant_id, :uuidandidentity :sku, :string);Entity.identity/1is its string. Every part is required and immutable, and only a loneidentity :id, :uuidis generated (as a UUIDv7). - Field values are type-checked (
:invalid_type), and command/query fields are typed. DDD.Errorhasentity_id.update/2refuses a struct the framework did not produce, and an aggregate's pending events are part of its seal.- Nested lists are one
jsonbcolumn.{:list, Entity}(and arrays of nested entities or maps) used to be a Postgres array ofjsonb(jsonb[]); they are now a singlejsonbarray. Existing tables needALTER TABLE t ALTER COLUMN c TYPE jsonb USING to_jsonb(c); the example app has the migration. The migration generator reports the column as changed. postgrexis no longer a dependency of the library: the application picks its driver.ecto_sqlstays (the outbox callsrepo.query!).
Added
DDD.Supervisor,DDD.Config.DDD.Query(read-only transactions),DDD.Filter,DDD.Listing,DDD.Page,DDD.Slice.- Field types:
:text,:time,:naive_datetime,:binary,:json,{:array, t},{:enum, [...]},{:map, t},{:ref, Aggregate},:money(DDD.Money). - Optimistic locking (
version,:stale_aggregate,retry_on_conflict:),DDD.Projection,DDD.Health,DDD.Telemetry. DDD.MCP: the application as a Model Context Protocol server (mix vic.mcp).DDD.Dialect: the few database-specific operations (claim lock, database clock, read-only transaction, column introspection) behind a behaviour chosen from the repo's adapter (DDD.Dialect.Postgres), or set withconfig :vicander, dialect: MyDialect. The outbox, dead letters, retention, projections, telemetry and health are Ecto queries overDDD.Outbox.EventandDDD.Outbox.Deliveryinstead of raw SQL. Event ids that the outbox functions (DDD.EventHandler.deliver/4, the poller'sclaim/3) take and return are now the UUID string, not the 16-byte binary. Leases and retry times use the database clock.DDD.Migration.drift/3looks at the schemas on the search path, not at every table of that name.DDD.Outbox.Retention(mix vic.outbox.prune,retention:of the supervisor).DDD.Test:assert_event_emitted/3,refute_event_emitted/3,assert_error_code/2,drain_outbox/1.descriptionon commands, queries, entities and fields; value specifications bound to a field (invariant Spec, on: :field).- The formatter configuration exports the DSL (
import_deps: [:vicander]). - Development checks: Credo (
mix credo --strict) and Dialyzer (mix dialyzer), both dev-only dependencies, run in CI.
Fixed
- Delivery of an event no longer crashes the poller; a failing event is retried and then dead-lettered. A handler whose nested transaction rolled back is retried.
- Blank strings are stored as they are (not as NULL); a malformed id is "not found".