16

DRY, SRP, IoC, DI, and Service Contracts

Keep Zheta Forge changeable by assigning one reason to change per service and injecting implementations behind explicit contracts.

Run it in the public monorepo

This course is built around the public Zheta Kubernetes Lab monorepo. The excerpt below is runnable source, not pseudocode.

Source: services/control_plane/app.py

def build_controller() -> ForgeController:
    database = Database(os.getenv("CONTROL_DATABASE_URL", "sqlite:///.lab/control.db"))
    repository = SqlProjectRepository(JsonProjectRepository(database))
    artifact_store = S3ArtifactStore(os.environ["ARTIFACT_BUCKET"]) if os.getenv("ARTIFACT_BUCKET") else SqlArtifactStore(Database(os.getenv("ARTIFACT_DATABASE_URL", "sqlite:///.lab/artifacts.db")))
    outbox = OutboxPublisher(database, publisher_from_url(os.getenv("BROKER_TOPIC", "http://broker:8080"), SERVICE_TOKEN)); outbox.start()
    forge = Forge(repository, GeneratorClient(os.getenv("GENERATOR_URL", "http://generator:8080")), RuntimeClient(os.getenv("RUNTIME_URL", "http://runtime:8080")), artifact_store, outbox)
    return ForgeController(forge)

Code to reality

Declared intent
Select concrete persistence, artifact, service-client, and event adapters at one visible composition root.
Interpreter
Python passes SERVICE_TOKEN into the broker publisher, starts the outbox relay, and injects the concrete object graph.
Software effect
Local broker HTTP or cloud SNS publishes transactional outbox events while the Forge model remains adapter-independent.
Hardware effect
The chosen URLs and stores decide which process, network endpoint, database, object storage, and broker consume resources.
Observable evidence
Exact source, adapter tests, a running outbox relay, broker delivery, and stable domain tests prove the wiring.

Start with the people and the result they need

The source tables below remain the detailed contract. Begin with these customer paths:

  • D16-UC-01
    • Person: Platform developer
    • Job: Add a storage implementation without changing application workflow rules
    • Observable result: All three archetypes use the same stable port while selecting tenant-safe adapters by environment
  • D16-UC-02
    • Person: Service owner
    • Job: Change one responsibility without redeploying unrelated services
    • Observable result: Release, grant, runtime, and generated-app owners change independently behind versioned contracts

Turn each customer job into a testable story

Now turn each customer job into a story with a result that an engineer can check:

  • D16-US-01
    • Story: As a platform developer, I want domain logic to depend on a storage port, so that local PostgreSQL and AWS implementations can change without…
    • Observable acceptance: Same contract suite passes for allowed adapters, rejects tenant leakage, and records implementation, environment, run, and trace
  • D16-US-02
    • Story: As a service owner, I want each service to own one coherent capability and schema, so that a release-policy change does not force connector or…
    • Observable acceptance: Ownership map, versioned schema, consumer checks, rejected breaking change, and unaffected service probe are recorded

Add real state and observable proof

Finally trace each story through the system that owns its state and the evidence that proves the outcome:

  • D16-FLOW-01
    • Trigger: Developer selects the dev persistence adapter
    • Responsible systems: ForgeController, Forge model, repository/artifact ports, SQLite and S3 adapters, tenant policy
    • Authoritative state: Control-plane database for projects; artifact database or S3 for artifacts
    • Owned record: ServiceContract
    • Observable evidence: Actor, contract version, adapter, tenant scope, expected and observed outcomes, environment, timestamp, commit, run, and trace IDs
    • Failure signal: Adapter constructed inside Forge, semantic mismatch, or cross-tenant read
  • D16-FLOW-02
    • Trigger: Service owner proposes a breaking event or API schema
    • Responsible systems: Control-plane, generator, runtime, evidence HTTP contracts; transactional outbox and broker ports
    • Authoritative state: Each service database plus durable outbox; SNS/SQS or SQLite broker delivery state
    • Owned record: AdapterBinding
    • Observable evidence: Rejected schema diff, impacted consumers, denial, prior response, environment, timestamp, commit, and run ID
    • Failure signal: Cyclic dependency, two writers, failed HTTP/auth contract, exhausted outbox relay, or unrelated service regression

The enterprise problem and today’s slice

Enterprise problem: A platform that can generate Research Brief, Service Desk, and Field Inspection applications becomes fragile when shared code means shared ownership, one service controls unrelated jobs, or business logic constructs databases and cloud clients directly. Whole-course context: The customer lifecycle now reaches verified export and retirement; today uses that evidence to draw internal boundaries that can evolve without breaking the lifecycle. Today’s slice: We apply Don’t Repeat Yourself (DRY), Single Responsibility Principle (SRP), Inversion of Control (IoC), Dependency Injection (DI), and contract tests to the control plane, runtime, and generated applications. End-of-day evidence: A service map, interface contract, positive contract test, rejected incompatible implementation, ownership decision, environment, timestamp, source revision, run, and trace IDs show replaceability. Still unsolved: Kubernetes packaging, infrastructure ownership, Terraform, and AWS isolation remain deferred.

Customer use cases

Design principles matter only when they protect customer outcomes, so boundaries should follow reasons to change rather than fashionable service names. These use cases test reuse and independent replacement across all three archetypes.

Use case IDActorCustomer jobSuccess outcomeDenial or recovery evidence
D16-UC-01Platform developerAdd a storage implementation without changing application workflow rulesAll three archetypes use the same stable port while selecting tenant-safe adapters by environmentAdapter with incompatible semantics or missing tenant enforcement fails contract tests before deployment
D16-UC-02Service ownerChange one responsibility without redeploying unrelated servicesRelease, grant, runtime, and generated-app owners change independently behind versioned contractsConsumer compatibility gate rejects breaking schema; prior implementation remains deployable

Actor-centred user stories

Copying less code is not enough if one shared module couples unrelated releases. These stories define DRY as one authoritative decision and DI as supplied capability, not hidden construction.

Story IDUse case IDsUser storyObservable acceptance conditions
D16-US-01D16-UC-01As a platform developer, I want domain logic to depend on a storage port, so that local PostgreSQL and AWS implementations can change without rewriting customer workflowsSame contract suite passes for allowed adapters, rejects tenant leakage, and records implementation, environment, run, and trace
D16-US-02D16-UC-02As a service owner, I want each service to own one coherent capability and schema, so that a release-policy change does not force connector or runtime changesOwnership map, versioned schema, consumer checks, rejected breaking change, and unaffected service probe are recorded

End-to-end product flows

Hidden dependencies turn a small customer request into an unpredictable graph of constructors and side effects. The flow resolves dependencies at the application boundary and exercises behavior through contracts.

Flow IDUse case IDsPathTriggerNumbered stepsTerminal evidence
D16-FLOW-01D16-UC-01HappyDeveloper selects the dev persistence adapter1. Composition root creates the adapter. 2. Inject it through a declared port. 3. Domain service executes tenant-safe workflow. 4. Contract suite compares outcomes. 5. Record adapter and evidence.Actor, contract version, adapter, tenant scope, expected and observed outcomes, environment, timestamp, commit, run, and trace IDs
D16-FLOW-02D16-UC-02DeniedService owner proposes a breaking event or API schema1. Publish candidate schema. 2. Run provider and consumer contracts. 3. Detect incompatible field or semantic change. 4. Block release. 5. Probe prior service as positive control.Rejected schema diff, impacted consumers, denial, prior response, environment, timestamp, commit, and run ID

The developer changes an implementation while the customer-visible workflow stays stable. That is the practical promise the design principles must prove.

System design derived from the flows

Service boundaries fail when they merely mirror technical layers or every function becomes a network call. Use SRP for one reason to change, DRY for one authoritative rule, and IoC/DI where replacing an external capability has real value.

Use case IDEntry pointResponsible servicesAuthoritative storeFailure evidence
D16-UC-01services/control_plane/app.py composition root and tests/test_domain.pyForgeController, Forge model, repository/artifact ports, SQLite and S3 adapters, tenant policyControl-plane database for projects; artifact database or S3 for artifactsAdapter constructed inside Forge, semantic mismatch, or cross-tenant read
D16-UC-02Python service-contract reviewControl-plane, generator, runtime, evidence HTTP contracts; transactional outbox and broker portsEach service database plus durable outbox; SNS/SQS or SQLite broker delivery stateCyclic dependency, two writers, failed HTTP/auth contract, exhausted outbox relay, or unrelated service regression

The real Python composition root in services/control_plane/app.py constructs ForgeController, injects a repository, generator client, runtime client, artifact store, and outbox into the Forge model, and then exposes an HTTP Handler. This is a small MVC split: domain.py is the model, controller.py translates authenticated commands, and app.py is both transport/view and composition root. DI is the constructor wiring; IoC is the process entry point choosing concrete adapters. The root passes SERVICE_TOKEN to publisher_from_url(...) and calls outbox.start(). Locally, http://broker:8080 is the real broker transport; in cloud, the required BROKER_TOPIC is SNS and subscriber delivery is SQS.

Data model and ownership

An interface can hide ownership confusion if two adapters both mutate the same record. Contracts therefore include record owner, identity scope, idempotency, and lifecycle semantics.

Generated-application database: Required in this slice — each generated application owns its tenant domain model, while injected adapters provide persistence without moving that authority into the control plane.

Record or entityStore and ownerPrimary keyForeign key or opaque referenceTenant keyMaterial constraintLifecycle and deletionUse case IDs
ServiceContractSchema Registry, owned by Platform Architecturecontract_name plus semantic_versionOpaque owning_service_idNone — contract is globally addressable but scoped by ownerOne owner; compatibility policy and authorization semantics are explicitDraft, approve, deprecate with deadline, retain versions for auditD16-UC-01, D16-UC-02
AdapterBindingEnvironment Configuration Store, owned by Runtime Platformbinding_idOpaque contract version and workload identity refsenvironment_idExactly one active adapter per port, application, and environmentCreate, validate, activate, roll back, retire with environmentD16-UC-01
DomainRecordGenerated-application database, owned by generated appdomain_record_idOpaque source and workflow refsapp_tenant_idOnly domain owner mutates; tenant predicate is mandatoryCreate, update, export, retain, and erase by application lifecycleD16-UC-01, D16-UC-02

Contract versions progress through explicit lifecycle states, and rejection leaves the prior customer workflow observable. This makes “replaceable” a tested property rather than a diagram label.

Inject an adapter at one composition root

Domain code that constructs AWS clients cannot run locally without cloud coupling and cannot expose its required authority. This labelled excerpt reflects the public monorepo’s actual Python ports and composition root at services/control_plane/app.py and services/control_plane/adapters.py.

def build_controller() -> ForgeController:
    database = Database(os.getenv("CONTROL_DATABASE_URL", "sqlite:///.lab/control.db"))
    repository = SqlProjectRepository(JsonProjectRepository(database))
    artifact_store = S3ArtifactStore(os.environ["ARTIFACT_BUCKET"]) if os.getenv("ARTIFACT_BUCKET") else SqlArtifactStore(Database(os.getenv("ARTIFACT_DATABASE_URL", "sqlite:///.lab/artifacts.db")))
    outbox = OutboxPublisher(database, publisher_from_url(os.getenv("BROKER_TOPIC", "http://broker:8080"), SERVICE_TOKEN)); outbox.start()
    forge = Forge(repository, GeneratorClient(os.getenv("GENERATOR_URL", "http://generator:8080")), RuntimeClient(os.getenv("RUNTIME_URL", "http://runtime:8080")), artifact_store, outbox)
    return ForgeController(forge)
Effect fieldWhat happens
Declared intentForge behavior receives repository, service clients, artifact storage, and outbox implementations at one composition root
InterpreterPython imports concrete adapters and constructs the object graph when the control-plane process starts
Software effectLocal URLs select SQL stores and the authenticated HTTP broker; ARTIFACT_BUCKET selects S3; the started outbox relays committed events
Hardware effectThe selected adapter consumes local or AWS CPU, memory, storage, and network resources
Evidencetests/test_domain.py, restart/tenant tests, adapter configuration, denied cross-tenant case, outbox delivery, and source revision

Key takeaways

Good boundaries preserve customer behavior while allowing one owned implementation to change.

  • DRY centralises decisions, not every similar line.
  • SRP follows reasons to change and ownership.
  • IoC and DI make dependencies explicit but never bypass authorization.

Checklist

A service boundary is credible only when ownership and compatibility are testable.

  • [ ] Each record and policy has one mutating owner
  • [ ] Ports state tenant, identity, idempotency, and failure semantics
  • [ ] Composition occurs at a small number of visible roots
  • [ ] Breaking contract and unaffected positive control are proven