Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1573c18
feat(pubsub): ✨ Add consumer middleware pipeline
haskou Jun 25, 2026
9646a79
feat(pubsub): ✨ Add publisher hook extension points
haskou Jun 25, 2026
1af5d0c
feat(ui): ✨ Extend Express kernel server hooks
haskou Jun 25, 2026
e204c85
fix(types): 🐛 Support classic TypeScript resolution
haskou Jun 25, 2026
7510b59
fix(kernel): 🐛 Harden integration extension points
haskou Jun 25, 2026
918b7d3
docs(readme): 📝 Remove stability section
haskou Jun 25, 2026
b287bcc
test(coverage): ✅ Cover integration extension branches
haskou Jun 25, 2026
884b9fb
docs(example): 📝 Polish application bootstrap
haskou Jun 25, 2026
e5d3aae
feat(kernel): ✨ Configure DI build from kernel
haskou Jun 25, 2026
d1c1c01
fix(example): 🐛 Fix example lint issues
haskou Jun 25, 2026
ebc0553
feat(express): ✨ Add HTTP pipeline registration API
haskou Jun 25, 2026
4daf57a
docs(example): 📝 Explain application bootstrap
haskou Jun 25, 2026
bafb23b
feat(di): ✨ Add container dependency overrides
haskou Jun 25, 2026
f5d689d
fix(di): 🐛 Resolve external package override references
haskou Jun 25, 2026
a0f228a
feat(express): ✨ Add shared HTTP error handler
haskou Jun 25, 2026
d42cc1c
test(coverage): ✅ Restore 100 percent coverage
haskou Jun 25, 2026
3d9fe64
fix(express): 🐛 Use application HTTP runtime dependencies
haskou Jun 25, 2026
0ee5200
chore(deps): ⬆️ Add Renovate and update dependencies
haskou Jun 25, 2026
55ab225
fix(review): 🐛 Address PR feedback
haskou Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .c8rc.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"branches": 95,
"branches": 100,
"check-coverage": true,
"exclude": ["src/index.ts", "src/**/index.ts"],
"functions": 95,
"functions": 100,
"include": ["src/**/*.ts"],
"lines": 95,
"statements": 95
"lines": 100,
"statements": 100
}
15 changes: 5 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![codecov](https://codecov.io/gh/haskou/ddd-kernel/branch/main/graph/badge.svg)](https://codecov.io/gh/haskou/ddd-kernel)
[![npm](https://img.shields.io/npm/v/@haskou/ddd-kernel.svg)](https://www.npmjs.com/package/@haskou/ddd-kernel)
[![license](https://img.shields.io/npm/l/@haskou/ddd-kernel.svg)](LICENSE)
[![Renovate](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com)

Framework-agnostic DDD kernel for TypeScript applications and microservices.

Expand Down Expand Up @@ -44,12 +45,6 @@ Constructor injection is the preferred application pattern. Direct service
lookup remains available for compatibility and integration boundaries, but it is
not the primary dependency model.

## Stability

This project is still in the `0.x` line. The current API is intentionally small
and covered by tests, but breaking changes may still happen while the kernel is
being extracted and hardened from production service patterns.

## Documentation

Usage guides, adapter authoring notes and API reference pages are published at:
Expand All @@ -65,10 +60,10 @@ CI publishes npm versions from pull requests merged into the default branch
according to the source branch prefix:

| Branch prefix | npm version bump |
| --- | --- |
| `fix/*` | Patch |
| `feat/*` | Minor |
| `break/*` | Major |
| ------------- | ---------------- |
| `fix/*` | Patch |
| `feat/*` | Minor |
| `break/*` | Major |

Other branch names run validation only and do not publish.

Expand Down
7 changes: 7 additions & 0 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ MongoDB repositories require:
```bash
yarn add mongodb
```

## TypeScript Resolution

The package publishes ESM, CommonJS and declaration files for every public
subpath. Modern projects should prefer `moduleResolution: "NodeNext"` or
`"Bundler"`, but declaration mappings are also provided for projects still using
classic `moduleResolution: "node"`.
89 changes: 89 additions & 0 deletions docs/guides/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,92 @@ export default class MyPublisher implements DomainEventPublisher {

If an adapter needs a third-party dependency, expose it through a subpath and
mark that dependency as an optional peer dependency.

## Choosing Adapters Per Runtime

Applications can keep several adapters for the same contract and choose one at
bootstrap time with dependency injection overrides. This is useful for tests,
local development or deployments that swap infrastructure without changing the
domain code.

```ts
await kernel.dependencyInjection({
overrides: [
{
token: UserRepository,
useClass:
process.env.NODE_ENV === 'test'
? InMemoryUserRepository
: MongoUserRepository,
},
],
});
```

For tests, overriding with a specific instance keeps assertions simple:

```ts
const users = new InMemoryUserRepository();

await kernel.dependencyInjection({
overrides: [
{
token: UserRepository,
useValue: users,
},
],
});
```

The classes that need `UserRepository` should still receive it through
constructor injection. The adapter decision belongs in bootstrap or test setup,
not inside consumers, schedulers or routes.

## Message Bus Hooks

Message bus adapters can expose publisher hooks so applications can attach
replicated publishers, websocket notifications, tracing or auditing without
wrapping the adapter in an application-local class.

```ts
import AmqpMessageBusAdapter from '@haskou/ddd-kernel/adapters/pubsub/amqp';

const messageBus = new AmqpMessageBusAdapter({
publisherHookErrorPolicy: {
handleAfterPublishError(error, context) {
logger.error(
`Post-publish hook failed for ${context.topic}: ${String(error)}`,
);
},
shouldFailAfterPublish() {
return false;
},
},
publisherHooks: [
{
afterPublish: async ({ domainEvent, message }) => {
await websocketPublisher.publish(domainEvent ?? message);
},
},
],
});
```

Custom generic adapters should implement the `MessageBus` contract. Domain-event
adapters should implement `DomainMessageBus`. Both can delegate hook execution
through `PublisherHookPipeline`:

```ts
import {
PublisherHookPipeline,
type PublisherHook,
} from '@haskou/ddd-kernel/adapters/pubsub';

export default class CustomMessageBus {
private readonly hooks = new PublisherHookPipeline();

public registerPublisherHooks(...hooks: PublisherHook[]) {
this.hooks.register(...hooks);
}
}
```
5 changes: 5 additions & 0 deletions docs/guides/amqp-pubsub.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ new AmqpMessageBusAdapter({
dsn: 'amqp://localhost',
exchange: 'users-service',
maxRetries: 3,
publisherHooks: [replicatedPublisherHook],
retryDelayInMilliseconds: 1000,
serviceName: 'users-service',
});
Expand All @@ -30,3 +31,7 @@ Environment variables:

Failed messages are sent to `<queue>_dlx`. Use `consumeDlx` to retry failed
messages.

`publisherHooks` run around each domain event published by the adapter. Use them
for transport-adjacent fan-out such as websocket updates, replicated-state
publishers, tracing or audit logs.
13 changes: 13 additions & 0 deletions docs/guides/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ export default class UserByIdFinder {
You normally do not call `registerFactory`. The generated `services.yaml` is the
composition metadata.

Build the container explicitly from the kernel when you want to regenerate that
metadata:

```ts
await kernel.dependencyInjection({
containerBuild: process.env.NODE_ENV !== 'production',
});
```

If `containerBuild` is omitted, the kernel falls back to
`CONTAINER_BUILD=true`. That keeps older applications working while allowing new
bootstraps to keep the choice close to application startup code.

Avoid passing `Kernel.di` into consumers, schedulers or services as a normal
dependency. It makes tests depend on global container state and hides the real
collaborators a class needs.
39 changes: 36 additions & 3 deletions docs/reference/consumer.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,44 @@ correlation IDs around handler execution:

```ts
kernel.registerConsumerMiddleware({
async handle(event, next) {
async handle(event, next, context) {
logger.info(`Handling ${context.eventName}`);
await next();
},
});
```

Middleware receives the event and a `next` callback. The kernel does not include
a full outbox or idempotency implementation.
Middleware receives the event, the next pipeline callback and a
`ConsumerExecutionContext` containing queue, exchange, event id, correlation id
and causation id. Transport adapters can also attach metadata, such as AMQP
headers or retry counts, to `context.metadata`.

## Built-in Middleware

The pub/sub adapter package includes small middleware implementations for common
consumer concerns. They are intentionally infrastructure-level primitives, not a
full outbox implementation.

```ts
import {
CorrelationConsumerMiddleware,
IdempotencyConsumerMiddleware,
InMemoryIdempotencyStore,
RetryConsumerMiddleware,
} from '@haskou/ddd-kernel/adapters/pubsub';

kernel.registerConsumerMiddleware(
new CorrelationConsumerMiddleware(),
new IdempotencyConsumerMiddleware({
store: new InMemoryIdempotencyStore(),
}),
new RetryConsumerMiddleware({
maxAttempts: 3,
}),
);
```

Use a custom `IdempotencyStore` for durable idempotency. Prefer stores that
implement atomic `claim`, `commit` and `release` methods so duplicate messages
cannot pass a non-atomic `has`/`mark` check concurrently. The in-memory store is
only useful for tests and single-process applications.
84 changes: 84 additions & 0 deletions docs/reference/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,87 @@ await di.compile();
```

Most applications call `kernel.dependencyInjection()` instead.

## Overrides

Use dependency overrides when the application wants a different implementation
for a contract in a specific runtime, such as replacing a Mongo repository with
an in-memory repository in tests.

Overrides are applied after `services.yaml` is loaded or generated and before
the container is compiled. That makes them win over the aliases generated from
abstract parents.

```ts
await kernel.dependencyInjection({
overrides: [
{
token: UserRepository,
useClass: InMemoryUserRepository,
},
],
});
```

`token` is the contract or class consumers ask the container for. `useClass` is
the implementation that should be returned instead.

Tests can also provide an already built instance:

```ts
const users = new InMemoryUserRepository();

await kernel.dependencyInjection({
overrides: [
{
token: UserRepository,
useValue: users,
},
],
});
```

Factories are useful when the replacement has local setup:

```ts
await kernel.dependencyInjection({
overrides: [
{
token: UserRepository,
useFactory: () => new InMemoryUserRepository(seedUsers),
},
],
});
```

Prefer constructor injection in services, consumers, schedulers and routes. The
override belongs at application bootstrap or test setup, not inside the class
that needs the dependency.

## External Package Contracts

`node-dependency-injection` can encode constructor dependencies imported from
external packages as unresolved service references. For example, a dependency
imported from `@haskou/ddd-kernel/domain` can appear in generated container
metadata as if it were a local path under the application source tree.

Overrides also cover those unresolved references. If an argument reference ends
with the overridden token class name, the container aliases that reference to
the configured override implementation:

```ts
import { DomainEventPublisher } from '@haskou/ddd-kernel/domain';
import MessageBus from '@haskou/ddd-kernel/adapters/pubsub/amqp';

await kernel.dependencyInjection({
overrides: [
{
token: DomainEventPublisher,
useClass: MessageBus,
},
],
});
```

This avoids local bridge contracts or hand-written aliases when applications
inject contracts exported by this package.
Loading
Loading