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 path
  • merge - Shallow merge an object into the target
  • append - Add an element to an array
  • remove - Remove an element from an array
  • increment - Add to a numeric value
  • decrement - Subtract from a numeric value

Array operations

Work on arrays of objects (e.g., line items, members, tags).

  • filter - Keep only elements matching a predicate
  • map - Transform each element in an array
  • update_where - Update elements that match a predicate
  • upsert - Update a matching element or append if none match
  • append_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 path
  • merge_at - Merge into a dynamic key path
  • remove_at - Remove a dynamic key
  • increment_at - Increment a value at a dynamic key path

Control flow

  • conditional - If/then/else branching with predicates
  • let - 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

Can't find what you need? support@j17.app