Evaluate: should start_link be excluded from behaviour callbacks? #3

Open
opened 2026-05-08 00:53:37 +00:00 by rodin · 1 comment
Owner

Question

When defining a behaviour for a module that wraps a GenServer, should start_link/1 be included as a @callback?

Argument against including it

start_link is called by the supervisor, which is configured with the concrete module directly ({QuoteFeed, opts} or {QuoteFeed.Fake, opts}). Nobody calls start_link polymorphically through the behaviour interface.

Behaviours exist to define the interface that callers use — the functions that application code dispatches through. subscribe/2 and unsubscribe/2 are genuine polymorphic interfaces. start_link is lifecycle boilerplate.

Including it adds noise to the contract without enabling any real polymorphism that the supervision tree doesn't already handle.

Argument for including it

  • Completeness: the behaviour documents the full public API
  • Supervisor configuration helpers (e.g., child_spec/1 wrappers) might want to call start_link through a module variable
  • Consistency: if it's public, it's in the contract

If we decide yes (exclude start_link)

  1. Update patterns/behaviours.md with guidance
  2. Audit gargoyle's existing behaviours for consistency
  3. Ensure Fake modules and any other implementors are updated

Context

Came up during PR #662 (QuoteFeed @behaviour). The PR currently includes start_link as a callback. If this becomes a best practice, we'd remove it and update the pattern.

## Question When defining a behaviour for a module that wraps a GenServer, should `start_link/1` be included as a `@callback`? ## Argument against including it `start_link` is called by the **supervisor**, which is configured with the concrete module directly (`{QuoteFeed, opts}` or `{QuoteFeed.Fake, opts}`). Nobody calls `start_link` polymorphically through the behaviour interface. Behaviours exist to define the interface that **callers** use — the functions that application code dispatches through. `subscribe/2` and `unsubscribe/2` are genuine polymorphic interfaces. `start_link` is lifecycle boilerplate. Including it adds noise to the contract without enabling any real polymorphism that the supervision tree doesn't already handle. ## Argument for including it - Completeness: the behaviour documents the full public API - Supervisor configuration helpers (e.g., `child_spec/1` wrappers) might want to call `start_link` through a module variable - Consistency: if it's public, it's in the contract ## If we decide yes (exclude start_link) 1. Update `patterns/behaviours.md` with guidance 2. Audit gargoyle's existing behaviours for consistency 3. Ensure Fake modules and any other implementors are updated ## Context Came up during PR #662 (QuoteFeed @behaviour). The PR currently includes `start_link` as a callback. If this becomes a best practice, we'd remove it and update the pattern.
Author
Owner

Evaluation approach

Search popular/robust Elixir applications and identify what they do:

  • Oban — defines behaviours for workers, plugins, engines. Does start_link appear in their callbacks?
  • Broadway — producer/processor behaviours. Same question.
  • Ecto — adapter behaviour (Ecto.Adapter). Does it include lifecycle functions?
  • Phoenix — endpoint, channel, socket behaviours.
  • Commanded — aggregate, event handler, process manager behaviours.
  • Nerves — hardware abstraction behaviours.
  • Horde — distributed supervisor/registry behaviours.

Look at what the ecosystem has settled on through practice, not just theory. If mature libraries consistently exclude start_link from their behaviour definitions, that's strong evidence for the convention.

## Evaluation approach Search popular/robust Elixir applications and identify what they do: - **Oban** — defines behaviours for workers, plugins, engines. Does `start_link` appear in their callbacks? - **Broadway** — producer/processor behaviours. Same question. - **Ecto** — adapter behaviour (`Ecto.Adapter`). Does it include lifecycle functions? - **Phoenix** — endpoint, channel, socket behaviours. - **Commanded** — aggregate, event handler, process manager behaviours. - **Nerves** — hardware abstraction behaviours. - **Horde** — distributed supervisor/registry behaviours. Look at what the ecosystem has settled on through practice, not just theory. If mature libraries consistently exclude `start_link` from their behaviour definitions, that's strong evidence for the convention.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rodin/elixir-patterns#3