Scriba.Projection behaviour (Scriba v0.1.0)

Copy Markdown View Source

Behaviour + use macro for the user-facing projection module.

See architecture doc §4.1 for the canonical example. The shortest form:

defmodule MyApp.Projections.Orders do
  use Scriba.Projection,
    name: "orders",
    source: {Scriba.Source.Commanded, application: MyApp.CommandedApp},
    target: {Scriba.Target.Ecto, repo: MyApp.Repo},
    parallelism: 16

  def handle(%OrderPlaced{} = event, _meta) do
    {:insert, %OrderReadModel{id: event.order_id, ...}}
  end

  def handle(_event, _meta), do: :skip
end

Then:

{:ok, _pid} = Scriba.start_projection(MyApp.Projections.Orders)

Required options

  • :name — projection's logical identity. String. Per architecture §5, do NOT encode version in the name (e.g. "orders_v1"); use the separate :version option for that. The macro emits a compile-time warning when :name matches _v<integer>$.
  • :source{module, opts} tuple. The module must implement Scriba.Source (and Broadway's Producer).
  • :target{module, opts} tuple. The module must implement Scriba.Target.
  • :parallelism — positive integer. Number of Broadway processors; also the modulus for the per-stream partition hash. No default — this is a real performance decision that should be deliberate.

Optional options (with defaults)

  • :version1. Bump to run a new projection side-by-side with the old one (two modules, two version integers).
  • :partition_by:stream_id. Only :stream_id is supported in v0.1. Custom partitioners are post-v0.1; passing anything else raises at compile time.
  • :handler__MODULE__. The module whose handle/2 clauses the engine calls. Defaults to the module being used.
  • :batch_size, :batch_timeout, :retry — pass through to Pipeline and the retry layer respectively. See Scriba.Projection.Pipeline and SCRIBA_ARCHITECTURE.md §9.1 for defaults.

handle/2 callback contract

See architecture §4.2 for the six valid return shapes:

  • :skip
  • {:insert, schema_struct}
  • {:update, schema_module, filter, [set: changes]}
  • {:delete, schema_module, filter}
  • {:multi, %Ecto.Multi{}}
  • {:error, reason} (routes to dead-letter)

Raising is also valid and treated like {:error, exception} (with the exception struct and stacktrace preserved for the dead-letter row). Retries apply to both shapes unless retry: false.

No catch-all is generated — decided, not overlooked

Commanded.Event.Handler generates def handle(_event, _metadata), do: :ok, so events a projector did not match were silently ignored. Scriba generates nothing: an unmatched event raises FunctionClauseError, dead-letters, and advances the cursor.

On an :all subscription that means a migrated projector without a final def handle(_event, _meta), do: :skip will dead-letter every unrelated event in the system. That is loud and recoverable — the dead-letter table is the first thing MIGRATION.md tells you to watch, and error_kind names the cause — where the alternative fails silently and looks like a working projection over an incomplete read model.

Loud is the v0.1 choice. It is also the one that stays open: an opt-in on_unmatched: :skip can be added in any 0.1.x without breaking anyone, whereas changing the default to skip would silently convert recorded failures into nothing.

Summary

Callbacks

Handles a single event. The engine calls this once per event in per-stream order. See module doc for the valid return shapes.

Callbacks

handle(event_data, meta)

@callback handle(event_data :: term(), meta :: map()) :: term()

Handles a single event. The engine calls this once per event in per-stream order. See module doc for the valid return shapes.