Skip to main content

README.md

# Counterpoint

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](https://dcb.events) 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

| Module | Role |
|---|---|
| `Counterpoint.Event` | Domain event: serialisable struct stored in the log |
| `Counterpoint.Command` | Reads events, validates, appends new ones |
| `Counterpoint.CommandWithEffect` | Like `Command` but with external deps injected |
| `Counterpoint.OnDemandProjection` | Folds events into state at query time |
| `Counterpoint.Projection` | Simpler fold without limit/reverse support |
| `Counterpoint.Query` | Composable filter by event types and tags |

## A worked example

### 1. Define an event

```elixir
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.

```elixir
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:

```elixir
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.

```elixir
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:

```elixir
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:

```elixir
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

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

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

Optional extras:

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