Implications

Last updated about 14 hours ago

Implications are reactive: when one event happens, they trigger another. All implied events are written atomically with the trigger event. Order placed? Reserve inventory. Low stock? Reorder. User upgraded? Grant features.

This guide covers practical patterns. For full syntax details, see Implications reference.

Basic implication

Define in your spec:

{
  "aggregate_types": {
    "order": {
      "events": {
        "was_placed": {
          "schema": {"type": "object", "properties": {"items": {"type": "array"}}},
          "handler": [{"set": {"target": "", "value": "$.data"}}],
          "implications": [
            {
              "emit": {
                "aggregate_type": "notification",
                "id": "admin",
                "event_type": "was_queued",
                "data": {"message": "New order received"}
              }
            }
          ]
        }
      }
    }
  }
}

When order.was_placed fires, j17 emits notification.admin.was_queued with the given data.

Conditional implications

Only trigger when conditions are met:

{
  "was_paid": {
    "schema": {"type": "object", "properties": {"amount": {"type": "number"}}},
    "handler": [{"set": {"target": "", "value": "$.data"}}],
    "implications": [
      {
        "condition": {"gte": ["$.data.amount", 100]},
        "emit": {
          "aggregate_type": "loyalty",
          "id": "$.metadata.actor.id",
          "event_type": "had_points_credited",
          "data": {"points": 10}
        }
      }
    ]
  }
}

Orders of $100 or more credit loyalty points. Conditions use the same predicate syntax as handlers.

Cross-aggregate implications

Implications can target different aggregate types:

{
  "user": {
    "events": {
      "had_tier_upgraded": {
        "schema": {"type": "object", "properties": {"new_tier": {"type": "string"}}},
        "handler": [{"set": {"target": "tier", "value": "$.data.new_tier"}}],
        "implications": [
          {
            "condition": {"equals": ["$.data.new_tier", "premium"]},
            "emit": {
              "aggregate_type": "features",
              "id": "$.metadata.actor.id",
              "event_type": "had_premium_enabled",
              "data": {"enabled_at": "$.metadata.timestamp"}
            }
          }
        ]
      }
    }
  }
}

User upgrade triggers a feature flag event on a different aggregate.

Dynamic target IDs

Implications have access to all standard event paths ($.key, $.id, $.type, $.data.*, $.metadata.*, @.*). Use them to route implied events dynamically:

{
  "emit": {
    "aggregate_type": "user_timeline",
    "id": "$.metadata.actor.id",
    "event_type": "had_activity_added",
    "data": {"event": "$.key", "type": "$.type"}
  }
}

State-based conditions

Access the source aggregate's current state in conditions:

{
  "condition": {"equals": ["@.notifications_enabled", true]},
  "emit": {
    "aggregate_type": "notification",
    "id": "$.metadata.actor.id",
    "event_type": "was_queued",
    "data": {}
  }
}

When accessing @.* in implications, you get the aggregate's state before any events in the current request are applied (S0). In a batch write, all implications in the batch see the same pre-batch state. The trigger event's data is available via $.data.*. ($.state.* is a deprecated alias for @.*.)

Fan-out with map

Emit one event per item in an array:

{
  "map": {
    "in": "$.data.items",
    "as": "$item",
    "emit": {
      "aggregate_type": "inventory",
      "id": "$item.product_id",
      "event_type": "was_reserved",
      "data": {
        "quantity": "$item.qty",
        "order_id": "$.key"
      }
    }
  }
}

Map with condition filter

Only emit for items matching a condition:

{
  "map": {
    "in": "$.data.items",
    "as": "$item",
    "condition": {"equals": ["$item.priority", "high"]},
    "emit": {
      "aggregate_type": "warehouse",
      "id": "$item.product_id",
      "event_type": "needs_urgent_pick",
      "data": {"qty": "$item.qty"}
    }
  }
}

Data template operators

For complex data transformations, use DSL operators in your data template.

concat - String concatenation

{
  "data": {
    "message": {"concat": ["Order ", "$.key", " was placed"]}
  }
}

coalesce - First non-null value

{
  "data": {
    "name": {"coalesce": ["$.data.display_name", "$.data.name", "Unknown"]}
  }
}

merge - Shallow object merge

{
  "data": {"merge": [
    "@.defaults",
    {"override": "value", "timestamp": "$.metadata.timestamp"}
  ]}
}

Later values override earlier ones. Operators can be nested (e.g., a merge element can contain a coalesce), up to 32 levels deep.

Scheduled implications

Delay the triggered event. See the scheduled events guide for full details.

{
  "was_placed": {
    "implications": [
      {
        "schedule": {
          "delay": "24h",
          "emit": {
            "aggregate_type": "notification",
            "id": "$.metadata.actor.id",
            "event_type": "cart_abandonment_reminder",
            "data": {"cart_id": "$.key"}
          },
          "cancel_on": [
            {
              "aggregate_type": "cart",
              "id": "$.key",
              "event_type": "was_checked_out"
            }
          ]
        }
      }
    ]
  }
}

If the cart is checked out within 24 hours, the reminder is automatically canceled.

Safety limits

Implications have built-in protection against runaway chains:

Limit Default Description
max_depth 5 Maximum chain depth (A implies B implies C implies D implies E)
max_total 100 Maximum total implied events per trigger
Template nesting 32 Maximum depth of nested template operators (merge/concat/coalesce)

Exceeding limits returns an error; the entire transaction (trigger + implied) is rejected.

The engine detects cycles at spec validation time. A spec with A:e1 implying B:e2 implying A:e1 will be rejected.

Audit trail

All implied events include metadata for traceability:

{
  "implied_by": {
    "key": "order:abc123",
    "event_type": "was_placed",
    "depth": 1
  }
}

Ordering and consistency

Implied events are written within microseconds of the trigger event, but they do not guarantee they will be the next event on the target aggregate's stream. Another write to the same target aggregate could land between the trigger and the implied event. Design your implied event handlers to be additive (set fields, append to arrays) rather than dependent on being exactly the N+1th event.

The hash chain on each aggregate stream remains intact regardless of interleaving — chain hashes are computed at write time from the actual previous event, not from what the implication expected.

Error handling

Implication failures don't block the original event. The parent event succeeds, implications are retried separately.

Common patterns

Inventory management

{
  "aggregate_types": {
    "order": {
      "events": {
        "was_placed": {
          "handler": [{"set": {"target": "", "value": "$.data"}}],
          "implications": [
            {
              "map": {
                "in": "$.data.items",
                "as": "$item",
                "emit": {
                  "aggregate_type": "inventory",
                  "id": "$item.sku",
                  "event_type": "had_reservation_requested",
                  "data": {
                    "order_id": "$.key",
                    "quantity": "$item.quantity"
                  }
                }
              }
            }
          ]
        }
      }
    },
    "inventory": {
      "events": {
        "had_reservation_requested": {
          "handler": [{"decrement": {"target": "available", "value": "$.data.quantity"}}],
          "implications": [
            {
              "condition": {"gte": ["@.available", "$.data.quantity"]},
              "emit": {
                "aggregate_type": "inventory",
                "id": "$.key",
                "event_type": "was_reserved",
                "data": {"quantity": "$.data.quantity"}
              }
            },
            {
              "condition": {"lt": ["@.available", "$.data.quantity"]},
              "emit": {
                "aggregate_type": "order",
                "id": "$.data.order_id",
                "event_type": "had_backorder_created",
                "data": {"sku": "$.key", "quantity": "$.data.quantity"}
              }
            }
          ]
        }
      }
    }
  }
}

Notifications

{
  "was_posted": {
    "handler": [{"set": {"target": "", "value": "$.data"}}],
    "implications": [
      {
        "emit": {
          "aggregate_type": "notification",
          "id": "$.data.post_author_id",
          "event_type": "was_created",
          "data": {
            "type": "new_comment",
            "post_id": "$.data.post_id",
            "commenter": "$.metadata.actor.id"
          }
        }
      }
    ]
  }
}

Audit logging

{
  "was_created": {
    "handler": [{"set": {"target": "", "value": "$.data"}}],
    "implications": [
      {
        "emit": {
          "aggregate_type": "audit",
          "id": "global",
          "event_type": "had_entry_added",
          "data": {
            "action": "create",
            "target": "$.key",
            "actor": "$.metadata.actor",
            "timestamp": "$.metadata.timestamp"
          }
        }
      }
    ]
  }
}

Testing implications

Write events in staging, verify implications fire:

# Place order
curl -X POST https://myapp-staging.j17.dev/order/123/was_placed \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"data": {"items": [{"sku": "widget_1", "quantity": 2}]}}'

# Check inventory reservation fired
curl https://myapp-staging.j17.dev/inventory/widget_1 \
  -H "Authorization: Bearer $API_KEY"

Compared to sagas

Implications Sagas
Trigger Automatic on event Explicit (trigger event)
Scope Single event chain Multi-step workflow
Compensation Manual (write compensating events) Built-in
Use case Simple reactions Complex business processes

Use implications for simple if-this-then-that. Use sagas for multi-step workflows with compensation.

When not to use implications

You need immediate confirmation -- Implications are atomic with the trigger, but if you need the implied event's response before returning to the client, use a saga.

Complex conditional logic -- More than 3-4 conditions? Consider a WASM handler or saga.

External calls needed -- Both Tick and WASM implications are pure data transformations with no network calls or I/O. Fetch external data before writing the trigger event.

See also

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