Skip to content

Events

Events are immutable records of things that happened in your system. They’re stored permanently in per-aggregate streams with monotonically increasing version numbers. Events are the source of truth — everything else (aggregate state, projections, read models) is derived from them.

var result = await hapnd.Aggregate("order_123")
.Append(new OrderPlaced { CustomerId = "cust_456" });
Console.WriteLine($"Event {result.EventId} at version {result.Version}");
const result = await hapnd
.aggregate("order_123")
.append("OrderPlaced", { customerId: "cust_456" });
console.log(`Event ${result.eventId} at version ${result.version}`);

Append multiple events atomically — all succeed or none do. This is essential for maintaining consistency when a single action produces multiple events.

var result = await hapnd.Aggregate("order_123")
.AppendMany([
new ItemAdded { Item = "Widget", Price = 25.00m },
new ItemAdded { Item = "Gadget", Price = 15.00m }
]);
Console.WriteLine($"Versions {result.StartVersion} to {result.EndVersion}");
const result = await hapnd.aggregate("order_123").appendMany([
{ eventType: "ItemAdded", data: { item: "Widget", price: 25.0 } },
{ eventType: "ItemAdded", data: { item: "Gadget", price: 15.0 } },
]);
console.log(`Versions ${result.startVersion} to ${result.endVersion}`);

The event type is a string discriminator stored with each event. Your reducers and projections use it to determine how to process the event.

The SDK resolves the event type from the class name automatically:

// Event type: "OrderPlaced"
await hapnd.Aggregate("order_123").Append(new OrderPlaced { ... });
// Event type: "ItemAdded"
await hapnd.Aggregate("order_123").Append(new ItemAdded { ... });

Override with the [EventType] attribute when the class name doesn’t match your domain language:

using Hapnd.Client;
[EventType("item_added")]
public class AddItemToOrder
{
public string Item { get; init; }
public decimal Price { get; init; }
}
// Event type: "item_added"
await hapnd.Aggregate("order_123").Append(new AddItemToOrder { ... });

In TypeScript, the event type is always passed explicitly as the first argument:

await hapnd
.aggregate("order_123")
.append("ItemAdded", { item: "Widget", price: 25.0 });

Use ExpectVersion to prevent lost updates when multiple processes write to the same aggregate concurrently. The server rejects the append if the aggregate’s current version doesn’t match.

try
{
await hapnd.Aggregate("order_123")
.ExpectVersion(5)
.Append(new ItemAdded { Item = "Widget", Price = 25.00m });
}
catch (HapndConcurrencyException ex)
{
Console.WriteLine($"Expected version {ex.ExpectedVersion}, actual {ex.ActualVersion}");
// Reload state and retry with business logic
}
import { HapndConcurrencyError } from "@hapnd/client";
try {
await hapnd
.aggregate("order_123")
.expectVersion(5)
.append("ItemAdded", { item: "Widget", price: 25.0 });
} catch (err) {
if (err instanceof HapndConcurrencyError) {
console.log(`Expected ${err.expectedVersion}, actual ${err.actualVersion}`);
// Reload state and retry with business logic
}
}

Use ExpectVersion(0) (or expectVersion(0)) to assert the aggregate doesn’t exist yet — useful for creation commands.

Group related operations across services. Typically propagated from an incoming HTTP request:

await hapnd.Aggregate("order_123")
.WithCorrelation(correlationId)
.Append(new ItemAdded { Item = "Widget" });
await hapnd
.aggregate("order_123")
.withCorrelation(correlationId)
.append("ItemAdded", { item: "Widget" });

Link events in a cause-and-effect chain. Set to the EventId of the event that triggered this operation:

var first = await hapnd.Aggregate("order_123")
.Append(new OrderPlaced { CustomerId = "cust_456" });
await hapnd.Aggregate("order_123")
.WithCausation(first.EventId)
.Append(new PaymentRequested { Amount = 50.00m });
const first = await hapnd
.aggregate("order_123")
.append("OrderPlaced", { customerId: "cust_456" });
await hapnd
.aggregate("order_123")
.withCausation(first.eventId)
.append("PaymentRequested", { amount: 50.0 });

Attach arbitrary context to events. Stored alongside the event but not part of the event data itself:

await hapnd.Aggregate("order_123")
.WithMetadata(new
{
UserId = "user_456",
IpAddress = "192.168.1.1",
UserAgent = "MyApp/1.0"
})
.Append(new OrderPlaced { CustomerId = "cust_456" });
await hapnd
.aggregate("order_123")
.withMetadata({
userId: "user_456",
ipAddress: "192.168.1.1",
userAgent: "MyApp/1.0",
})
.append("OrderPlaced", { customerId: "cust_456" });