Counterpoint applies the UNIX philosophy to the backend: small, autonomous feature slices that compose through a shared stream of events.

The relational database is the usual culprit for inter-feature coupling. Shared tables, foreign keys, and God-schemas make it hard to change one feature without touching another. The standard event-sourcing response is to define a stream per aggregate and enforce it as the consistency boundary but that trades one constraint for another: now your business rules must fit inside a single stream.

DCB - Dynamic Consistency Boundaries dissolves that constraint. Rather than routing all writes through a fixed stream, commands declare exactly which events they care about and the store guarantees consistency against those events only. Features stay autonomous; their event queries compose freely across the shared log.

Counterpoint is the Elixir layer on top of DCB: events, commands, projections, and automations : each scoped to a feature slice, composing through the event stream.

NB: Aggregates are still a valid way to group commands : you just aren't forced into them. And because the log is the source of truth and streams are defined at query time, your model can evolve freely as you discover it.

lib/my_app/
 orders/
    commands/
       place_order.ex      # validates + appends OrderPlaced
    events/
       order_placed.ex     # immutable fact
    views/
        order_summary.ex    # folds events → %OrderSummary{}
 inventory/
    ...

Core building blocks

ModuleRole
Counterpoint.EventDomain event: serialisable struct stored in the log
Counterpoint.CommandReads events, validates, appends new ones
Counterpoint.CommandWithEffectLike Command but with external deps injected
Counterpoint.OnDemandProjectionFolds events into state at query time
Counterpoint.ProjectionSimpler fold without limit/reverse support
Counterpoint.QueryComposable filter by event types and tags

A worked example

1. Define an event

defmodule MyApp.Orders.Events.OrderPlaced do
  use Counterpoint.Event

  defstruct [:order_id, :total, :occurred_at]

  def tags(%__MODULE__{order_id: id}), do: ["order_id:#{id}"]
  def to_map(%__MODULE__{order_id: id, total: t, occurred_at: ts}),
    do: %{"order_id" => id, "total" => t, "occurred_at" => DateTime.to_iso8601(ts)}
  def from_map(%{"order_id" => id, "total" => t, "occurred_at" => ts}),
    do: %__MODULE__{order_id: id, total: t, occurred_at: ts}
end

2. Write a command

The command reads state, enforces a rule, and appends an event if all is well. Optimistic concurrency is built in: if a concurrent write lands between your read and append, the runner retries automatically.

defmodule MyApp.Orders.Commands.PlaceOrder do
  use Counterpoint.Command
  import Counterpoint.ReadAppender
  alias Counterpoint.Query
  alias MyApp.Orders.Events.OrderPlaced

  defstruct [:order_id, :total]

  @impl Counterpoint.Command
  def run(%__MODULE__{order_id: id, total: total}, ra) do
    {existing, ra} =
      read_events(ra, Query.new() |> Query.add_item(types: [OrderPlaced], tags: ["order_id:#{id}"]))

    if Enum.any?(existing) do
      {:error, :already_placed}
    else
      append_event(ra, %OrderPlaced{order_id: id, total: total, occurred_at: DateTime.utc_now()})
    end
  end
end

Execute it:

Counterpoint.CommandRunner.run(:my_store, %PlaceOrder{order_id: "ord-1", total: 99})

3. Build a read model

Projections are just a query + a fold. No persistence layer needed for in-memory reads.

defmodule MyApp.Orders.Views.OrderSummary do
  use Counterpoint.OnDemandProjection
  alias Counterpoint.Query
  alias MyApp.Orders.Events.OrderPlaced

  defstruct [:order_id, :total]

  @impl Counterpoint.OnDemandProjection
  def query(order_id),
    do: Query.new() |> Query.add_item(types: [OrderPlaced], tags: ["order_id:#{order_id}"])

  @impl Counterpoint.OnDemandProjection
  def init, do: %__MODULE__{}

  @impl Counterpoint.OnDemandProjection
  def apply(state, %Counterpoint.Envelope{data: %OrderPlaced{order_id: id, total: t}}),
    do: %{state | order_id: id, total: t}
end

Query it:

Counterpoint.OnDemandProjection.run(MyApp.Orders.Views.OrderSummary, :my_store, "ord-1")
# => %OrderSummary{order_id: "ord-1", total: 99}

Wiring it up

Add to your supervision tree:

def start(_type, _args) do
  children = [
    {Counterpoint.Supervisor,
     store: [name: :my_store, namespace: "my_app"],
     events: [MyApp.Orders.Events.OrderPlaced]}
  ]
  Supervisor.start_link(children, strategy: :one_for_one)
end

Projections beyond in-memory

On-demand in-memory projections (above) cover most read needs. For continuous read models — updating a Postgres table, a search index, or a cache as events arrive — use automations: background workers that watch the event log and react to new events. See Counterpoint.Automation for details.

Installation

def deps do
  [
    {:counterpoint, "~> 0.1.0"}
  ]
end

Requires Elixir 1.18+ and a running FoundationDB cluster for the DCB event store.

Optional extras:

{:oban, "~> 2.18"}   # for the Oban queue adapter (distributed automations)
{:plug,  "~> 1.16"}  # for the HTTP integration helpers