Handlers
Handlers transform aggregate state when events occur. They're pure functions: (state, event) -> new_state.
What handlers do
When you query an aggregate, j17: 1. Loads all events for that aggregate 2. Applies handlers in chronological order 3. Returns the final state
Event 1: was_created { name: "Alice" }
-> Handler: set name -> State: { name: "Alice" }
Event 2: had_email_updated { email: "alice@example.com" }
-> Handler: merge email -> State: { name: "Alice", email: "..." }
Final state: { name: "Alice", email: "alice@example.com" }
Declarative vs imperative
Imperative (traditional code):
javascript
function applyEvent(state, event) {
if (event.type === 'was_created') {
state.name = event.data.name;
state.created_at = Date.now();
} else if (event.type === 'had_email_updated') {
state.email = event.data.email;
}
return state;
}
Problems: - Hidden logic (Date.now() is a side effect) - Hard to test (mock Date) - Runtime errors (undefined properties)
Declarative (j17 Tick):
json
{
"was_created": {
"handler": [
{ "set": { "target": "", "value": "$.data" } },
{ "set": { "target": "created_at", "value": "$.metadata.timestamp" } }
]
}
}
Benefits: - No side effects (timestamp comes from event metadata) - Easy to test (input -> output) - Validated at upload (typos caught early)
Tick operations
Tick provides 17 declarative operations in four categories:
Basic operations
The foundation. Cover most use cases.
set- Replace value at a target pathmerge- Shallow merge an object into the targetappend- Add an element to an arrayremove- Remove an element from an arrayincrement- Add to a numeric valuedecrement- Subtract from a numeric value
Array operations
Work on arrays of objects (e.g., line items, members, tags).
filter- Keep only elements matching a predicatemap- Transform each element in an arrayupdate_where- Update elements that match a predicateupsert- Update a matching element or append if none matchappend_unique- Append only if no matching element exists
Dynamic key operations
Operate on keys determined at runtime from event data.
set_at- Set a value at a dynamic key pathmerge_at- Merge into a dynamic key pathremove_at- Remove a dynamic keyincrement_at- Increment a value at a dynamic key path
Control flow
conditional- If/then/else branching with predicateslet- Bind a variable (e.g., find an element) for use in subsequent operations
See Tick reference for complete syntax.
Handler context
Handlers access:
| Path | Meaning |
|---|---|
$.data |
Event payload |
$.metadata.timestamp |
Event timestamp |
$.metadata.actor |
Who triggered the event |
$.state |
Current aggregate state (before this event) |
$.key |
Aggregate key (type/id) |
Common patterns
Initialize state
{
"was_created": {
"handler": [
{ "set": { "target": "", "value": "$.data" } },
{ "set": { "target": "status", "value": "active" } },
{ "set": { "target": "created_at", "value": "$.metadata.timestamp" } }
]
}
}
Update field
{
"had_email_updated": {
"handler": [
{ "set": { "target": "email", "value": "$.data.email" } }
]
}
}
Conditional update
{
"had_status_changed": {
"handler": [
{ "set": { "target": "status", "value": "$.data.status" } },
{
"if": { "eq": ["$.data.status", "completed"] },
"then": [
{ "set": { "target": "completed_at", "value": "$.metadata.timestamp" } }
]
}
]
}
}
Append to array
{
"had_item_added": {
"handler": [
{
"append": {
"target": "items",
"value": {
"$merge": [
{ "$": "$.data" },
{ "added_at": { "$": "$.metadata.timestamp" } }
]
}
}
}
]
}
}
Handler ordering
Handlers execute in the order defined:
{
"handler": [
{ "set": { "target": "status", "value": "processing" } },
{ "increment": { "target": "processing_count", "value": 1 } },
{ "append": { "target": "log", "value": { "action": "processed" } } }
]
}
Each operation sees the result of previous operations.
Validation
j17 validates handlers when you upload your spec:
- Target paths are valid
- Array operations target arrays
- Numeric operations target numbers
- JSONPath expressions parse
- No circular references
Invalid specs are rejected with line numbers.
Testing handlers
Test locally before deploying:
# Test specific handler
j17 handler test spec.json --aggregate user --event was_created --state '{}' --event-data '{"name":"Alice"}'
Or use the CLI dry-run mode:
j17 events dry-run spec.json events.jsonl
Performance
Tick handlers compile to Zig. Typical throughput: 300k+ operations/second per core.
See also
- Tick reference - Complete syntax
- Events - What triggers handlers
- Aggregates - What handlers build