Skip to main content

ADR-003: No mediator, no command/query bus

Status: accepted

Context

The previous iteration routed every command and query through a global Mediator with handlers registered by decorator at import time (@mediator.command(...)), which required import handlers # noqa in the bootstrap for the side effect. It also split every operation into command class + handler + view model.

MediatR-style buses solve real problems - cross-cutting pipelines (logging, validation, transactions), decoupled handlers, many entrypoints - at the cost of indirection you cannot follow with "go to definition".

Decision

  • A use case is a class with a constructor and an execute method. The router calls it.
  • A command object exists only when an operation has several input fields (CreateTournamentCommand). Single-id operations take the id.
  • Reads and writes go through the same repository until a read genuinely needs a different shape (then: a read-model port, see Extending).

Consequences

  • Every call is explicit and greppable; a new reader can trace a request in one sitting.
  • Cross-cutting concerns are handled where they belong: transactions in the session dependency, error mapping in shared/http/errors.py, logging in the event bus.
  • No handler registry, no import-order magic, no "no handler found" runtime error.

When to revisit

  • Several entrypoints (HTTP, CLI, consumers) dispatch the same commands and you want one pipeline of behaviours around all of them.
  • You need a persistent audit of every command executed.
  • Reads dominate and their shape diverges from the aggregates: add read models first; a bus is still optional.

Proof

No executable proof: the decision is the absence of a pattern. There is no runtime property to falsify; use cases being plain classes is visible in application/use_cases.py.