Tactical Domain-Driven Design patterns library for .NET 8+
Based on Java ezddd 6.0.1
A modern tactical DDD library for .NET with event sourcing, state sourcing, and CQRS patterns. This is a faithful .NET port of the Java ezddd 6.0.1 library (GitLab commit: 3aac0f5) with ~99% semantic parity and .NET-specific improvements.
ezDDD.NET follows Semantic Versioning and is published on NuGet β the current version is shown by the NuGet badge above, and you can install it from the Quick Start. Changes between releases are tracked in CHANGELOG.md.
- Quick Start
- Features
- Modules
- API Overview - Key types (Complete API Docs β)
- Examples - Minimal example (30+ More Examples β)
- Requirements
- Differences from Java Version
- Documentation
- Contributing
- License
- References
dotnet add package ezDDD.CoreezDDD.Core is the all-in-one aggregator; individual modules (ezDDD.Common, ezDDD.Entity, ezDDD.UseCase, ezDDD.Cqrs) can also be installed separately β see Modules.
using EzDdd.Entity;
// Aggregate ID (ToString() drives the stream name: "account-ACC-001")
public sealed record AccountId(string Value) : IValueObject
{
public override string ToString() => Value;
}
// Domain events are immutable records
public sealed record AccountCreated(
Guid Id, DateTimeOffset OccurredOn, AccountId Source, string Owner, decimal InitialBalance
) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent
{
string IDomainEvent.Source => Source.Value;
IReadOnlyDictionary<string, string> IDomainEvent.Metadata => new Dictionary<string, string>();
}
public sealed record MoneyDeposited(
Guid Id, DateTimeOffset OccurredOn, AccountId Source, decimal Amount
) : IInternalDomainEvent
{
string IDomainEvent.Source => Source.Value;
IReadOnlyDictionary<string, string> IDomainEvent.Metadata => new Dictionary<string, string>();
}
// Event-sourced aggregate
public sealed class BankAccount : EsAggregateRoot<AccountId, IInternalDomainEvent>
{
public BankAccount(AccountId id, string owner, decimal initialBalance)
{
Id = id;
Apply(new AccountCreated(Guid.NewGuid(), DateTimeOffset.UtcNow, id, owner, initialBalance)); // R1
}
public BankAccount(IEnumerable<IInternalDomainEvent> events) : base(events) { } // event replay
public string Owner { get; private set; } = string.Empty;
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new InvalidOperationException("Deposit amount must be positive");
Apply(new MoneyDeposited(Guid.NewGuid(), DateTimeOffset.UtcNow, Id, amount)); // R2
}
protected override void _When(IInternalDomainEvent @event)
{
switch (@event)
{
case AccountCreated created:
Id = created.Source;
Owner = created.Owner;
Balance = created.InitialBalance;
break;
case MoneyDeposited deposited:
Balance += deposited.Amount;
break;
}
}
protected override void _EnsureInvariant()
{
if (Balance < 0)
throw new InvalidOperationException("Balance cannot be negative");
}
public override string GetCategory() => "account";
}This is a trimmed version of the compile-verified BankAccount example; the full version (with a Money value object, withdraw/close, and the EsRepository round-trip) is in USAGE_EXAMPLES.md.
- β
Tactical DDD building blocks:
IEntity<TId>,IValueObject,AggregateRoot<TId, TEvent>, domain events withMetadatafor idempotency and distributed tracing - β
Event sourcing:
EsAggregateRoot<TId, TEvent>enforcing R1/R2/R3 invariant rules via template method, with event replay and{category}-{id}stream naming - β
State sourcing:
OutboxRepositorypersists aggregate state + events atomically (Transactional Outbox) - β
Repository bridge pattern:
IRepository(domain abstraction) βIRepositoryPeer(persistence SPI; the transaction boundary) - β
CQRS:
ICommand/IQuery/IInquiry/IProjectionwith theCqrsOutput<T>fluent output API - β
Event reaction:
IReactor<TInput>hierarchy βIProjector<TInput>maintains read models,INotifier<TInput>converts internal events to external (integration) events - β
External publishing:
IExternalDomainEventPublisher<TEvent>out-port; repositories never publish β a separate Relay does (see examples/EventInfrastructure/) - β
System reconciliation:
IReconciler<TContext, TReport>for maintenance jobs (cleanup, consistency checks)
- π Async/await throughout: All I/O operations return
Task<T>, never blocking - π― Clean Architecture: Unidirectional dependencies (Common β Entity β UseCase β Cqrs), ports & adapters
- π¦ Zero external dependencies: Only .NET BCL + uContract.NET (ecosystem dependency)
- π Thread-safe: Concurrent collections,
Lazy<T>, and snapshot patterns - π Strongly typed: Generic variance (
in TInput,out TOutput) and nullable reference types - 𧬠Modern C# idioms: Records for events/value objects, pattern matching for event handlers
- π§ͺ Highly tested: 543 tests passing, >90% coverage across all modules
- π€ Semantic parity: ~99% parity with Java ezddd 6.0.1, upstream tracked per release
Five NuGet packages with a unidirectional dependency chain (Common β Entity β UseCase β Cqrs β Core):
| NuGet Package | Purpose | Depends on |
|---|---|---|
ezDDD.Common |
Foundation utilities (BiMap, JsonUtil, Converter) |
β |
ezDDD.Entity |
Core DDD patterns (entities, value objects, aggregates, domain events) | Common, uContract |
ezDDD.UseCase |
Use cases, repositories (event/state sourcing), event infrastructure | Entity |
ezDDD.Cqrs |
CQRS patterns (commands, queries, projections, CqrsOutput) |
UseCase |
ezDDD.Core β |
All-in-one aggregator (no code of its own) | All of the above |
Note: Package IDs use the
ezDDD.*prefix; namespaces useEzDdd.*(e.g.using EzDdd.Entity;).
π Complete Documentation: API_REFERENCE.md β signatures, parameters, exceptions, and examples for every public API
| Module | Key Types |
|---|---|
| Common | BiMap<TKey, TValue>, JsonUtil, Converter<TSource, TTarget> |
| Entity | IEntity<TId>, IValueObject, IDomainEvent, IInternalDomainEvent, AggregateRoot<TId, TEvent>, EsAggregateRoot<TId, TEvent>, DomainEventTypeMapper |
| UseCase | IUseCase<TInput, TOutput>, IReactor<TInput>, IReconciler<TContext, TReport>, IRepository<TAggregate, TId, TEvent>, IRepositoryPeer<TData, TId>, EsRepository<TAggregate, TId>, OutboxRepository<TAggregate, TData, TId>, IExternalDomainEventPublisher<TEvent>, ExitCode |
| Cqrs | ICommand<TInput, TOutput>, IQuery<TInput, TOutput>, IInquiry<TInput, TOutput>, IProjection<TInput, TOutput>, IProjector<TInput>, INotifier<TInput>, IArchive<TData, TId>, CqrsOutput<T> |
π More Examples: USAGE_EXAMPLES.md (30+ compile-verified scenarios)
- Basic Patterns: Aggregates, value objects, domain events β USAGE_EXAMPLES.md
- Event Sourcing: BankAccount aggregate, replay, R1/R2/R3 rules,
EsRepositoryβ USAGE_EXAMPLES.md - State Sourcing: Transactional Outbox,
OutboxRepository,OutboxMapperβ USAGE_EXAMPLES.md - CQRS: Commands, queries, projections,
CqrsOutputfluent API β USAGE_EXAMPLES.md - System Reconciliation: Cleanup reconcilers,
NullContext, scheduling β USAGE_EXAMPLES.md - Real-World Scenarios: Banking, e-commerce, inventory, order management β USAGE_EXAMPLES.md
- Relay Pattern: Reference implementation of
EventStoreRelay(event store β publisher) β examples/EventInfrastructure/
- .NET 8.0 or later (C# 12, nullable reference types enabled)
- uContract 1.0.0+ β Design by Contract support (same ecosystem as Java ezddd's uContract); used by
EsAggregateRootinvariant checking - No other dependencies β production code uses only .NET built-in APIs (
System.Text.Json,System.Reflection,System.Collections.Concurrent)
ezDDD.NET maintains ~99% semantic parity with Java ezddd 6.0.1 β core patterns (aggregates, R1/R2/R3 rules, repository bridge, Transactional Outbox, CQRS separation, reactor hierarchy) behave identically β while adopting .NET platform idioms.
| Aspect | Java ezddd | C# ezDDD.NET |
|---|---|---|
| Async | Synchronous execute(input) |
ExecuteAsync(input) returns Task<T>, non-blocking |
| Method naming | camelCase() |
PascalCase() + Async suffix for async methods |
| Protected overrides | when(), ensureInvariant() |
_When(), _EnsureInvariant() (underscore prefix) |
| Null safety | Optional<T>, @Nullable |
Nullable reference types (T?), compiler-enforced |
| Generics | <ID, E> |
<TId, TEvent> with variance (in / out) |
| Event handling | instanceof chains |
Pattern matching with switch |
| Immutability | final fields, getters |
record types with init properties |
| Serialization | Jackson | System.Text.Json (built-in) |
| Java ezddd | C# ezDDD.NET |
|---|---|
Optional<T> findById(ID) |
Task<T?> FindByIdAsync(TId) |
addDomainEvent(E) / getDomainEvents() |
_AddDomainEvent(TEvent) / GetDomainEvents() |
CqrsOutput.create().succeed() |
CqrsOutput<T>.Create().Succeed() |
MessageProducer (moved to ezddd-gateway in 6.0.0) |
Excluded from core, matching Java; a .NET Gateway package is deferred post-1.0 |
Java:
public class BankAccount extends EsAggregateRoot<AccountId, InternalDomainEvent> {
private Money balance;
public void deposit(Money amount) {
var event = new MoneyDeposited(UUID.randomUUID(), Instant.now(), id, amount);
apply(event);
}
@Override
protected void when(InternalDomainEvent event) {
if (event instanceof MoneyDeposited deposited) {
this.balance = balance.add(deposited.amount());
}
}
}C#:
public sealed class BankAccount : EsAggregateRoot<AccountId, IInternalDomainEvent>
{
private Money _balance = new(0);
public void Deposit(Money amount)
{
var @event = new MoneyDeposited(Guid.NewGuid(), DateTimeOffset.UtcNow, Id, amount);
Apply(@event);
}
protected override void _When(IInternalDomainEvent @event)
{
switch (@event)
{
case MoneyDeposited deposited:
_balance = _balance.Add(deposited.Amount);
break;
}
}
}See MIGRATION_GUIDE.md for side-by-side Java/C# comparisons of every pattern, syntax mapping tables, and common gotchas.
- π API_REFERENCE.md - Complete API reference
- Every public type and method with signatures, exceptions, and examples
- Verified against the shipped public API baseline
- π USAGE_EXAMPLES.md - Real-world examples
- 30+ compile-verified scenarios (banking, e-commerce, inventory, order management)
- Event sourcing, state sourcing, CQRS, and reconciliation walkthroughs
- π MIGRATION_GUIDE.md - Java β .NET migration guide
- π CHANGELOG.md - Release history (Keep a Changelog / SemVer)
- π¨βπ» AGENTS.md - Development standards and workflow (TDD, Tidy First, build/test commands)
- π docs/adr/ - Architecture Decision Records documenting design rationale
- πΊοΈ ROADMAP.md - Current status and post-1.0 considerations
Contributions are welcome! Please see CONTRIBUTING.md for development guidelines.
Before contributing:
- Read the Architecture Decision Records to understand design rationale
- Follow the standards in AGENTS.md β tests first (TDD), >90% coverage
MIT License β Copyright (c) 2025-2026 ezDDD.NET Contributors. See LICENSE for details.
This project is a derivative work of the Java ezddd library by Teddy Chen and contributors, which is licensed under the Apache License 2.0. See THIRD-PARTY-NOTICES.txt for the required attribution and license text.
This .NET port is based on Java ezddd 6.0.1 (GitLab commit: 3aac0f5; synchronized 2.1.0 β 4.1.0 β 6.0.1 before first publication)
- Repository: Java ezddd (GitLab) by Teddy Chen (TeddySoft)
- Ecosystem: uContract.NET - Design by Contract dependency
- Domain-Driven Design - Eric Evans, tactical DDD patterns
- Event Sourcing - Martin Fowler
- CQRS - Greg Young
- Clean Architecture - Robert C. Martin