@hexaijs/core
v0.9.0
Published
Core utilities/types/base classes for hexai projects
Downloads
914
Readme
@hexaijs/core
Core domain primitives for building hexagonal architecture applications
Overview
@hexaijs/core provides the foundational building blocks for domain-driven design in TypeScript applications. It establishes the core abstractions that all other @hexai packages build upon.
The package focuses on three key areas:
- Messaging - A unified
Messageabstraction for commands, queries, and events with built-in headers (id, type, timestamp, schema version) - Domain Modeling - Base classes for aggregates, entities, and domain events following DDD tactical patterns
- Infrastructure Interfaces - Abstract contracts for repositories, unit of work, and event stores that infrastructure packages implement
These primitives are intentionally minimal. They define contracts and patterns without prescribing implementation details, allowing you to integrate with any database, message broker, or framework.
Installation
npm install @hexaijs/coreCore Concepts
Message
Message is the base abstraction for all messages in the system - commands, queries, and events share this foundation.
import { Message } from "@hexaijs/core";
class CreateOrderCommand extends Message<{
customerId: string;
items: { productId: string; quantity: number }[];
}> {
static readonly type = "order.create-order";
}
// Create a message
const command = new CreateOrderCommand({
customerId: "customer-123",
items: [{ productId: "product-456", quantity: 2 }]
});
// Access message data
command.getMessageId(); // unique UUID
command.getMessageType(); // "order.create-order"
command.getTimestamp(); // Date when created
command.getPayload(); // the typed payload objectEvery message automatically receives headers including a unique ID and timestamp. The type static property follows the convention "bounded-context.message-name".
MessageOptions
The constructor accepts an optional MessageOptions object for passing custom headers:
interface MessageOptions {
headers?: Record<string, unknown>;
}
// Pass custom headers
const command = new CreateOrderCommand(
{ customerId: "customer-123", items: [] },
{ headers: { correlation: { id: "corr-abc", type: "HttpRequest" } } }
);Serialization
Messages provide two serialization methods:
// toJSON() - preserves Date objects, suitable for structured output
const json = command.toJSON();
// { headers: { id, type, createdAt: Date, ... }, payload: { ... } }
// serialize() - fully serialized plain object (dates become strings)
const plain = command.serialize();
// { headers: { id, type, createdAt: "2026-...", ... }, payload: { ... } }
// JSON.stringify uses toJSON() automatically
JSON.stringify(command);Override serializePayload() to customize how the payload is serialized:
class MyEvent extends DomainEvent<{ date: Temporal.PlainDate }> {
protected serializePayload(payload: { date: Temporal.PlainDate }) {
return { date: payload.date.toString() };
}
}Deserialization
Use the static from() method to reconstruct a message from serialized data:
const raw = { customerId: "c-123", items: [] };
const headers = { id: "msg-1", type: "order.create-order", createdAt: "2026-01-01T00:00:00Z" };
const command = CreateOrderCommand.from(raw, headers);Override deserializeRawPayload() for custom deserialization:
class MyEvent extends DomainEvent<{ date: Temporal.PlainDate }> {
protected static deserializeRawPayload(raw: any) {
return { date: Temporal.PlainDate.from(raw.date) };
}
}Fluent Header API
Use withHeader() to create a new message instance with an additional header:
const command = new CreateOrderCommand({ customerId: "c-123", items: [] })
.withCorrelation({ id: "corr-abc", type: "HttpRequest" })
.withHeader("source", "api-gateway");
command.getCorrelation(); // { id: "corr-abc", type: "HttpRequest" }withHeader() returns a new immutable instance - the original message is not modified.
Message Tracing
Messages support built-in correlation and causation tracing for distributed message flows:
import { Message, MessageTrace } from "@hexaijs/core";
// Get message identity as trace
const trace: MessageTrace = command.asTrace();
// { id: "msg-uuid", type: "order.create-order" }
// Set correlation (root message in chain)
const correlated = command.withCorrelation({ id: "req-123", type: "HttpRequest" });
correlated.getCorrelation(); // { id: "req-123", type: "HttpRequest" }
// Set causation (direct parent message)
const caused = event.withCausation(command.asTrace());
caused.getCausation(); // { id: "cmd-uuid", type: "order.create-order" }When events are published through ApplicationContext.publish(), causation and correlation headers are set automatically.
DomainEvent
DomainEvent extends Message for events that represent something that happened in your domain.
import { DomainEvent } from "@hexaijs/core";
export class OrderPlaced extends DomainEvent<{
orderId: string;
customerId: string;
totalAmount: number;
}> {
static readonly type = "order.order-placed";
}
// Create and use domain events
const event = new OrderPlaced({
orderId: "order-789",
customerId: "customer-123",
totalAmount: 150.00
});
// Check event type
if (message.getMessageType() === OrderPlaced.getType()) {
const payload = message.getPayload();
// Handle the event...
}Id and Identifiable
Value objects for identity, ensuring type safety for entity IDs.
import { Id, Identifiable } from "@hexaijs/core";
// Create a typed ID class
class OrderId extends Id<string> {}
// Use it in your domain
const orderId = new OrderId("order-123");
orderId.getValue(); // "order-123"
// Compare IDs
const sameId = new OrderId("order-123");
orderId.equals(sameId); // true
// Different ID types are not comparable
class CustomerId extends Id<string> {}
const customerId = new CustomerId("order-123");
// orderId.equals(customerId) - TypeScript prevents thisAggregateRoot
Base class for aggregate roots that collect domain events for later dispatch.
import { AggregateRoot, Id } from "@hexaijs/core";
class OrderId extends Id<string> {}
class Order extends AggregateRoot<OrderId> {
private status: "pending" | "confirmed" | "shipped" = "pending";
static create(orderId: OrderId, customerId: string): Order {
const order = new Order(orderId);
order.raise(new OrderPlaced({
orderId: orderId.getValue(),
customerId,
totalAmount: 0
}));
return order;
}
confirm(): void {
if (this.status !== "pending") {
throw new Error("Can only confirm pending orders");
}
this.status = "confirmed";
this.raise(new OrderConfirmed({
orderId: this.getId().getValue()
}));
}
}
// Usage
const order = Order.create(new OrderId("order-123"), "customer-456");
order.confirm();
// Events are collected, not immediately published
const events = order.getEventsOccurred();
// [OrderPlaced, OrderConfirmed]
// flushEvents() returns events AND clears the internal list
const flushed = order.flushEvents();
// [OrderPlaced, OrderConfirmed]
order.getEventsOccurred(); // [] — now emptyflushEvents() is useful in repository implementations where you need to publish events after persisting the aggregate, and want to ensure events are not accidentally re-published.
Repository
Interface for aggregate persistence. Implementations live in infrastructure packages.
import { Repository, ObjectNotFoundError } from "@hexaijs/core";
// Define your repository interface
interface OrderRepository extends Repository<Order> {
get(id: OrderId): Promise<Order>;
add(order: Order): Promise<void>;
update(order: Order): Promise<void>;
}
// Use in your domain/application layer
async function confirmOrder(
orderId: OrderId,
repository: OrderRepository
): Promise<void> {
const order = await repository.get(orderId);
order.confirm();
await repository.update(order);
}UnitOfWork
Interface for transaction management. The primary API is scope() for defining transaction boundaries.
import { UnitOfWork, Propagation } from "@hexaijs/core";
// UnitOfWork is typically accessed through application context
interface OrderApplicationContext {
getUnitOfWork(): UnitOfWork;
getOrderRepository(): OrderRepository;
}
// Define a transaction boundary with scope()
await unitOfWork.scope(async () => {
const order = Order.create(orderId, customerId);
await orderRepository.add(order);
});scope() vs wrap()
| Method | Signature | Status |
|--------|-----------|--------|
| scope(fn) | fn: () => Promise<T> | Recommended |
| wrap(fn) | fn: (client) => Promise<T> | Deprecated |
scope() defines a transaction boundary without exposing the database client. Client access is handled separately through infrastructure methods (e.g., withClient() in @hexaijs/postgres). This separation enables lazy transaction initialization — the actual BEGIN is deferred until the first client access.
Transaction Lifecycle Hooks
Register callbacks that execute at specific points in the transaction lifecycle. Hooks must be registered inside an active scope().
await unitOfWork.scope(async () => {
// Runs before COMMIT — if it throws, transaction rolls back instead
unitOfWork.beforeCommit(async () => {
await validateBusinessRules();
});
// Runs after successful COMMIT (best-effort)
unitOfWork.afterCommit(async () => {
await sendNotification();
});
// Runs after ROLLBACK (best-effort)
unitOfWork.afterRollback(async () => {
await cleanupResources();
});
await repository.save(order);
});Hook execution semantics:
| Hook | When | On failure |
|------|------|------------|
| beforeCommit | Before COMMIT, sequentially | Transaction rolls back instead of committing |
| afterCommit | After COMMIT, sequentially | Best-effort: all hooks run, errors collected into AggregateError |
| afterRollback | After ROLLBACK, sequentially | Best-effort: all hooks run, errors collected into AggregateError |
Hooks are scope-local — they are cleared after the transaction completes. Calling beforeCommit() / afterCommit() / afterRollback() outside a scope() throws an error.
Transaction Propagation
// Transaction propagation options
Propagation.NEW // Always start a new transaction
Propagation.EXISTING // Join existing transaction, or create new if none
Propagation.NESTED // Create a savepoint within current transactionEventStore
Interface for event sourcing scenarios. Stores and retrieves events by position.
import { EventStore, StoredEvent } from "@hexaijs/core";
// EventStore interface for reading events
interface MyEventStore extends EventStore {
fetch(afterPosition: number, limit?: number): Promise<{
events: StoredEvent[];
lastPosition: number;
}>;
}
// StoredEvent wraps event with its position
interface StoredEvent {
position: number;
event: Message;
}Error Types
The package provides standard domain error types:
import {
DomainError,
InvariantNotSatisfiedError,
ValidationError,
RepositoryError,
ObjectNotFoundError,
DuplicateObjectError
} from "@hexaijs/core";
// Domain invariant violations
throw new InvariantNotSatisfiedError(
"ORDER_ALREADY_SHIPPED",
"Cannot modify a shipped order"
);
// Field-level validation errors
throw new ValidationError(
"email",
"INVALID_FORMAT",
"Email must be a valid email address"
);
// Repository errors
throw new ObjectNotFoundError("Order not found");
throw new DuplicateObjectError("Order with this ID already exists");Error hierarchy:
DomainError- Base for all domain errorsInvariantNotSatisfiedError- Business rule violationsValidationError- Field-specific validation failures
RepositoryError- Base for persistence errorsObjectNotFoundError- Entity not foundDuplicateObjectError- Duplicate key/entity
API Highlights
| Export | Description |
|--------|-------------|
| Message<P> | Base message class with headers and typed payload |
| MessageOptions | Options for Message constructor ({ headers? }) |
| MessageTrace | Interface for message identity ({ id, type }) used in tracing |
| Message.asTrace() | Returns this message's identity as MessageTrace |
| Message.getCausation() | Gets the direct parent message trace |
| Message.getCorrelation() | Gets the root message trace in the chain |
| Message.withCausation(trace) | Sets causation, returns new instance |
| Message.withCorrelation(trace) | Sets correlation, returns new instance |
| DomainEvent<P> | Message subclass for domain events |
| AggregateRoot<T> | Base class for aggregates with event collection |
| AggregateRoot.flushEvents() | Returns collected events and clears the internal list |
| Id<T> | Value object for typed identities |
| Identifiable<T> | Interface for entities with identity |
| Repository<T> | Interface for aggregate persistence |
| UnitOfWork | Interface for transaction management (scope() for boundaries, wrap() deprecated) |
| TransactionHook | Type for hook callbacks: () => void \| Promise<void> |
| TransactionHooks | Reusable hook registry with commit/rollback lifecycle execution |
| Propagation | Enum for transaction propagation modes |
| EventStore | Interface for event store implementations |
See Also
- @hexaijs/application - Application layer with command handlers and context
- @hexaijs/postgres - PostgreSQL implementation of UnitOfWork and EventStore
- @hexaijs/sqlite - SQLite implementation for testing
