Skip to content

Design principles

The framework is built in layers, each with a clear job and clear borders. The goal is to keep the logic that decides what "supported" means separate from the tools used to prove it. Terraform, a cloud provider, an issue tracker or a chat app should be replaceable without rewriting the core.

Layered design of the frameworkFour nested layers: domain models at the centre, use cases around them, interfaces around those, and adapters for Terraform, the cloud, GitHub, trackers and chat on the outside. Dependencies point inward.AdaptersTerraform, cloud SDK, GitHub, tracker, Slack, Teams, storageInterfacesDetector, Provisioner, Validator, Publisher, Artifact storeUse casesDetect releases, validate trigger, resume teardownDomainTrigger, Topology, RunResult, SupportStateThe core never imports a tool. Tools are handed to it.

The four layers

Layer Holds In this framework
Domain Plain models of what a run is about, with no knowledge of storage, tools or APIs Trigger, Topology, RunResult, CheckResult, SupportState
Use cases The rules of the business: what happens, in what order, and what counts as a pass Detect new releases, validate a trigger end to end, resume teardown after triage
Interfaces The shape of everything the use cases need from the outside world Detector, Provisioner, Validator, Publisher, ArtifactStore
Adapters Concrete implementations of those interfaces The Terraform CLI, the cloud SDK, the GitHub API, the tracker, Slack, Teams, object storage

Tools sit at the edge because they're the parts most likely to be swapped; a Trigger or SupportState only changes when the meaning of "supported" does.

The dependency rule

One rule holds the layers together

The core never imports a tool. Tools are handed to it.

  • Entry points: the scheduler, a CLI command or a chat button starts a use case with a Trigger. Those callers sit at the edge and depend on the core, never the other way round.
  • Tools: the "validate trigger" use case needs something that can provision and something that can publish. It declares what it needs as a Provisioner and a Publisher, and the wiring code at start-up decides whether that means Terraform and Linear, or OpenTofu and Jira.
class ValidateTrigger:
    def __init__(self, provisioner: Provisioner, validators: ValidatorFactory,
                 publisher: Publisher, store: ArtifactStore):
        self.provisioner, self.validators = provisioner, validators
        self.publisher, self.store = publisher, store

    def run(self, trigger: Trigger) -> RunResult:
        self.publisher.publish(RunEvent.started(trigger))
        target = self.provisioner.apply(trigger)
        checks = [v.run(target) for v in self.validators.for_topology(trigger.topology)]
        result = RunResult(trigger, target, checks)
        self.store.save(result)
        if result.passed:
            self.provisioner.destroy(trigger)
        self.publisher.publish(RunEvent.passed(result) if result.passed else RunEvent.failed(result))
        return result

This class contains the whole lifecycle and none of the tooling. The Terraform details live in a TerraformProvisioner adapter, and the Jira or Linear details live in a tracker publisher.

Why it's worth the structure

Benefit What it means here
Replaceable tools Swap Terraform for OpenTofu or Pulumi, or Jira for Linear, by writing one adapter
One lifecycle, many topologies VM hosts, self-managed and managed clusters run the same use case with different adapters
Readable core The run lifecycle reads as the business process, not as API calls
Real tests in the right places The use case is checked against real adapters in the end-to-end run, and adapters are checked against their real tools

How the layers map to the framework

The framework layers describe the flow of a run: intake, configuration, orchestration, validation and publishing. This page describes the dependencies. Each stage of the flow is a use case or part of one, and every tool it touches sits behind an interface.

Flow stage Use case side Adapter side
Trigger intake The ledger decides what's new Scheduled image check, GitHub watcher, page check
Configuration generation Triggers become a run configuration The JSON writer for Terraform variables
Orchestration Provision, then decide when to destroy The Terraform CLI, remote state
Validation Which checks apply to a topology SSH, kubectl through the bastion, backend queries
Publishing Which events go where Tracker, docs, Slack, Teams, storage