ddd-tactical-core-boilerplate
v2.0.0
Published
Reusable TypeScript building blocks for tactical Domain-Driven Design
Downloads
277
Maintainers
Readme
DDD Tactical Core Boilerplate
A small, dependency-free TypeScript foundation for tactical Domain-Driven Design.
Build expressive domain models with entities, value objects, aggregates, events, commands, queries and repository ports—without coupling your core to a framework.
Why this package? · Quick start · API overview · Package compatibility · Contributing
Why this package?
Tactical DDD patterns are simple in isolation, but every team eventually has to answer the same questions: how should entities compare, how are domain events represented, what does a repository port return, and where do commands and queries belong?
DDD Tactical Core Boilerplate provides a compact, consistent answer while leaving your architecture and infrastructure choices open.
- Framework-agnostic — use it with Fastify, NestJS, Express, workers, CLIs or no framework at all.
- Focused tactical primitives — entities, aggregates, value objects, rules, events, messages, commands, queries and repository contracts.
- Deliberate domain state — immutable value objects, encapsulated entity mutation and explicit aggregate event lifecycles.
- Explicit failure handling — the
Either,okandfailhelpers keep expected domain failures out of exception-driven control flow. - Infrastructure stays at the edge — bus and repository interfaces let the domain depend on contracts rather than implementations.
- No runtime dependency chain — the published package has zero runtime dependencies.
- Modern package output — ESM, CommonJS, TypeScript declarations and source maps are built and validated before publication.
- Permissive licensing — freely use and adapt the package under the MIT Licence.
Installation
npm install ddd-tactical-core-boilerplateRequirements:
- Node.js 22 or newer
- TypeScript 4.8 or newer when consuming the generated declarations
Quick start
Create a validated value object without introducing a framework or validation dependency:
import { Domain, fail, ok, type Either } from 'ddd-tactical-core-boilerplate';
type EmailAddressProps = {
value: string;
};
class EmailAddress extends Domain.ValueObject<EmailAddressProps> {
private constructor(value: string) {
super({ value });
}
static create(input: string): Either<EmailAddress, Error> {
const value = input.trim().toLowerCase();
if (!value.includes('@')) {
return fail(new Error('A valid email address is required'));
}
return ok(new EmailAddress(value));
}
get value(): string {
return this.props.value;
}
}
const emailResult = EmailAddress.create('[email protected]');
if (emailResult.isOk()) {
console.log(emailResult.value.value);
} else {
console.error(emailResult.value.message);
}Continue the example by composing the value object into an aggregate:
class CustomerId extends Domain.Identifier<string> {
private constructor(value: string) {
super(value);
}
static from(value: string): CustomerId {
if (!value.startsWith('customer_')) {
throw new Error('Customer IDs must start with customer_');
}
return new CustomerId(value);
}
}
type CustomerProps = {
displayName: string;
email: EmailAddress;
};
class Customer extends Domain.Aggregate<CustomerProps, CustomerId> {
private constructor(props: CustomerProps, id: CustomerId) {
super(props, id);
}
static create(props: CustomerProps, id: CustomerId): Customer {
return new Customer(props, id);
}
static reconstitute(props: CustomerProps, id: CustomerId): Customer {
return new Customer(props, id);
}
rename(displayName: string): void {
this.props.displayName = displayName.trim();
}
}
if (emailResult.isFail()) {
throw emailResult.value;
}
const customer = Customer.create(
{
displayName: 'Ada Lovelace',
email: emailResult.value,
},
CustomerId.from('customer_01JABCXYZ'),
);
console.log(customer.id.toString());API overview
The public API is deliberately grouped by architectural responsibility.
| Area | Included building blocks |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| Domain | Identifier, Entity, Aggregate, ValueObject, ReadModel, UUIDv4, domain events and rules |
| Domain.StandardVO | Standard value objects, currently including ISO currency values |
| Application | Use-case, command-handler and query-handler contracts; commands, queries and orchestrator handlers |
| Application.Repo | CRUD repository ports, update options, repository errors and the unexpected-error decorator |
| Infra.EventBus | Domain/integration event contracts, system events and event handlers |
| Infra.CommandBus | Publish/subscribe and stream command-bus contracts |
| Infra.QueryBus | Query-bus contracts |
| Infra.MessageBus | System-message bus contracts and subscriber handlers |
| Direct exports | Either, ok, fail, asyncLocalStorage and its store type |
Result handling
Either<Success, Failure> makes an expected failure part of a function's type:
import { fail, ok, type Either } from 'ddd-tactical-core-boilerplate';
function divide(dividend: number, divisor: number): Either<number, Error> {
if (divisor === 0) {
return fail(new Error('Cannot divide by zero'));
}
return ok(dividend / divisor);
}Domain state and identity
Value objects accept only plain domain data: primitives, arrays and plain objects.
The base class defensively copies and deeply freezes that data, then compares it
structurally. Date, Map, Set, class instances and circular structures should
be converted to explicit domain primitives first.
Entities and aggregates are deliberately stateful. Their properties are protected,
so changes belong in named domain methods such as rename() or confirm(). The
base constructor requires an identifier: it never silently replaces a supplied ID
or chooses UUIDv4 on behalf of the domain.
Use Domain.Identifier<string | number | bigint> for domain-specific IDs, or
extend Domain.UUIDv4 when UUIDv4 is genuinely the right strategy:
class InvoiceId extends Domain.UUIDv4 {}
const freshId: InvoiceId = InvoiceId.generate();
const restoredId = InvoiceId.fromString('b3bb718f-602d-4b1f-80b1-f891c54f6252');Aggregates queue events without writing to the console. domainEvents returns a
readonly snapshot; publish it and call clearDomainEvents() only after successful
dispatch. Persistence adapters can record a loaded version with
updateAggregateVersion().
Repository ports
Define storage contracts in the application layer and implement them wherever your infrastructure lives:
import type { Application, Either } from 'ddd-tactical-core-boilerplate';
interface CustomerView {
id: string;
displayName: string;
}
type CustomerReadRepository = Application.Repo.ICRUDReadPort<CustomerView>;
type CustomerWriteRepository = Application.Repo.ICRUDWritePort<Customer, CustomerId>;
type FindCustomerResult = Either<CustomerView, Application.Repo.Errors.NotFound>;Package compatibility
| Consumer | Entry point |
| -------------------- | ----------------------------------------------------- |
| ESM (import) | dist/index.mjs with dist/index.d.mts declarations |
| CommonJS (require) | dist/index.cjs with dist/index.d.cts declarations |
| Node.js | 22 or newer |
| TypeScript | 4.8 or newer for generated declarations |
| Runtime dependencies | None |
Both module paths expose the same runtime API. Package exports are checked with
publint and
Are the Types Wrong?.
Development
Clone the repository and run the complete validation pipeline:
git clone https://github.com/bitloops/ddd-tactical-core-boilerplate.git
cd ddd-tactical-core-boilerplate
npm ci
npm run checkUseful commands:
| Command | Purpose |
| --------------------------- | -------------------------------------------------------------- |
| npm run build | Build ESM, CommonJS, declarations and source maps |
| npm test | Build and exercise both module formats with Node's test runner |
| npm run typecheck | Check the source with TypeScript |
| npm run typecheck:package | Check the generated declarations as a package consumer |
| npm run lint | Run ESLint |
| npm run format | Format source and documentation with Prettier |
| npm run package:check | Validate package exports and declaration resolution |
| npm run check | Run every required local verification step |
Upgrading from 1.x
Version 2 makes its identity and mutation contracts explicit. Supply both generic
arguments to Domain.Entity<Props, Id> and Domain.Aggregate<Props, Id>, and pass
an ID to super(props, id). Replace direct value-object property access with
domain accessors, clearEvents() with clearDomainEvents(), and assignments to
aggregateVersion with updateAggregateVersion(version). UUIDv4 inputs are now
validated; use a Domain.Identifier subtype for non-UUID IDs.
Contributing
Issues, ideas and pull requests are welcome.
- Check the issue tracker for related work.
- Keep changes focused and add tests for observable behaviour.
- Run
npm run checkbefore opening a pull request. - Explain the domain or architectural problem the change solves.
For larger API changes, please open an issue first so the design can be discussed before implementation.
Acknowledgements
Source adapted from other authors retains its original copyright and licence notices. See THIRD_PARTY_NOTICES.md for details.
Licence
Copyright © 2026 Bitloops S.A.
Released under the MIT Licence.
