Skip to content
SQA Cockpit

ADR-0001 - Three-layer architecture

ADRsUpdated 2 min readEdit on GitHub ↗

ADR-0001 - Three-layer architecture #

  • Status: Accepted (Context widened by ADR-0008; third-layer directory renamed apps/systems/ by ADR-0009)
  • Date: 2026-05-07
  • Deciders: Natan

Context #

The runner needs to verify multiple aspects of the Snappy stack across environments. Two competing pressures shape the layout:

  1. Capabilities outlive flows. "Is the S3 bucket reachable?" is a

question we'll ask in many contexts. The probe should live somewhere reusable.

  1. Flows are app-specific. "Run preflight, then data probes, then

behavioural tests" is unique to one app and doesn't generalise.

We considered three options:

  • Flat (one folder) - capability and composition code intermingled.

Quickly tangles; the next app copy-pastes.

  • Five layers (probes / checks / segments / flows / orchestrators)
    • boundaries blur, "where does this go?" gets two valid answers.
  • Three layers - lib (shared infra) → components (per-system

capabilities) → apps (compositions).

Decision #

Adopt three layers:

text
src/
├── lib/          shared infrastructure (logger, tracer, formatters)
├── components/   per-system capabilities (one folder per system)
└── apps/         flow compositions (one folder per app)

Each layer has a single responsibility. The decision tree:

  • Operates on no system in particular → lib/
  • Operates on exactly one external system → components/<system>/
  • Decides ordering / composes other things → apps/<app>/

Consequences #

  • Pro: One obvious answer for where any new code goes.
  • Pro: Components are reusable across apps (same s3.ready usable

from snappy-api and a future conveyor app).

  • Pro: Apps stay thin and declarative (read like a table of contents).
  • Con: Two-file minimum to add a probe (component + app wiring).

Acceptable - composition is the value.

  • Falsifiability: Revisit if either (a) we end up with components

that touch multiple systems, or (b) apps grow non-trivial logic outside step() calls. Either signals the wrong split.

See also #

explanation, with the decision tree and runtime composition.

Was this page helpful?