03

Draw the Three Planes and Split the Microservices

Derive service boundaries from customer flows and owned state instead of drawing a fashionable component catalogue.

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: compose.yaml

control-plane:
    build: { context: ., dockerfile: services/Dockerfile, args: { SERVICE: control_plane } }
    environment: { ENVIRONMENT: local, GENERATOR_URL: "http://generator:8080", RUNTIME_URL: "http://runtime:8080", EVIDENCE_URL: "http://evidence:8080", SERVICE_TOKEN: "local-service-token-32-characters-minimum", JWT_SECRET: "local-jwt-secret-32-characters-minimum", CONTROL_DATABASE_URL: "sqlite:////data/control.db", ARTIFACT_DATABASE_URL: "sqlite:////data/artifacts.db", BROKER_TOPIC: "http://broker:8080" }
    volumes: ["control-data:/data"]
    ports: ["8080:8080"]
    depends_on: [generator, runtime, evidence, broker]

Code to reality

Declared intent
Run six explicit local responsibilities: web, control plane, generator, runtime, broker, and evidence.
Interpreter
Docker Compose resolves the service graph, builds images, creates networks and volumes, and starts containers.
Software effect
The control plane calls generator and runtime, then its outbox publishes to the broker for the independently progressing evidence subscriber.
Hardware effect
Six containers and four persistent volumes consume laptop CPU, memory, storage, and network namespaces.
Observable evidence
docker compose ps proves web, control-plane, generator, runtime, broker, and evidence processes; restart queries prove four durable volumes.

Start with the people and the result they need

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

  • D03-UC-01
    • Person: Platform architect
    • Job: Assign each create-to-retire action to one service and store owner
    • Observable result: No record has two writers and every cross-service reference is explicit
  • D03-UC-02
    • Person: Security architect
    • Job: Keep provider, hosted-runtime, and generated-app grants separate
    • Observable result: Each plane authenticates, authorizes, audits, and revokes independently

Turn each customer job into a testable story

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

  • D03-US-01
    • Story: As a platform architect, I want one writer for every durable record, so that retries and failures cannot create competing truth
    • Observable acceptance: Context-map validation reports one service/store owner and explicit opaque references for every entity
  • D03-US-02
    • Story: As a security architect, I want tokens scoped to one authorization plane, so that control-plane membership cannot bypass app roles
    • Observable acceptance: Cross-plane token test is denied, audited, and paired with an unaffected valid request

Add real state and observable proof

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

  • D03-FLOW-01
    • Trigger: Architect submits the Zheta Forge context map
    • Responsible systems: Organization, Application Catalog, Generation Orchestrator, Preview Manager, Connector Broker, Data Provisioner, Release Controller, Audit Evidence,…
    • Authoritative state: Versioned context-map contract
    • Owned record: ServiceOwnershipContract
    • Observable evidence: Map hash, service, interface, record owner, plane, environment, timestamp, validation run ID
    • Failure signal: Multiple writers, ownerless record, circular dependency, or undocumented interface
  • D03-FLOW-02
    • Trigger: Control-plane credential calls a generated-app domain endpoint
    • Responsible systems: Identity service, Runtime Gateway, generated-app authorization adapter, Audit Evidence
    • Authoritative state: Plane-local grants and immutable audit event
    • Owned record: CrossPlaneAuthorizationEvidence
    • Observable evidence: Denied actor, audience, resource, policy, trace ID, plus successful positive-control trace
    • Failure signal: Wrong token audience, missing mapping, revoked grant, or absent denial trace

The enterprise problem and today’s slice

Enterprise problem: A single service that creates apps, holds connectors, deploys runtimes, and authorizes end users gives every change an oversized blast radius and makes data ownership impossible to review.

Whole-course context: The charter bounds the product and the local Kind substrate can run processes; today decides which process owns each responsibility before implementation begins.

Today’s slice: Split Zheta Forge into control-plane services, hosted-runtime controllers, and generated-application services using single responsibility, dependency inversion, and explicit interfaces.

End-of-day evidence: A versioned context map joins each customer action to one responsible service, authoritative record, interface, and independently revocable authorization plane.

Still unsolved: Organization creation, generation jobs, preview sandboxes, connectors, managed data, and release automation remain later vertical slices.

Customer use cases

Microservices add network and operational failure modes, so a split is justified only by ownership or independent change. These use cases force every boundary to earn its existence.

Use case IDActorCustomer jobSuccess outcomeDenial or recovery evidence
D03-UC-01Platform architectAssign each create-to-retire action to one service and store ownerNo record has two writers and every cross-service reference is explicitDuplicate writer or circular ownership fails the architecture check
D03-UC-02Security architectKeep provider, hosted-runtime, and generated-app grants separateEach plane authenticates, authorizes, audits, and revokes independentlyA provider token used at a generated-app endpoint is denied while a valid app user succeeds

Actor-centred user stories

Naming services without acceptance evidence merely moves ambiguity into boxes. These stories require executable ownership and authorization checks.

Story IDUse case IDsUser storyObservable acceptance conditions
D03-US-01D03-UC-01As a platform architect, I want one writer for every durable record, so that retries and failures cannot create competing truthContext-map validation reports one service/store owner and explicit opaque references for every entity
D03-US-02D03-UC-02As a security architect, I want tokens scoped to one authorization plane, so that control-plane membership cannot bypass app rolesCross-plane token test is denied, audited, and paired with an unaffected valid request

End-to-end product flows

A customer request crosses services only when responsibility changes. The happy path records an application intent; the denied path proves that identity does not flow implicitly between planes.

Flow IDUse case IDsPathTriggerNumbered stepsTerminal evidence
D03-FLOW-01D03-UC-01, D03-UC-02HappyArchitect submits the Zheta Forge context map1. Enumerate product actions.; 2. Assign one responsible service.; 3. Assign one authoritative store.; 4. Define synchronous or asynchronous interface.; 5. Validate dependency direction.Map hash, service, interface, record owner, plane, environment, timestamp, validation run ID
D03-FLOW-02D03-UC-01, D03-UC-02DeniedControl-plane credential calls a generated-app domain endpoint1. Runtime Gateway validates audience.; 2. Generated app rejects the wrong audience.; 3. Audit Evidence stores denial.; 4. Valid generated-app session repeats the request.Denied actor, audience, resource, policy, trace ID, plus successful positive-control trace

The architect begins with customer-visible actions. A service exists only where a responsibility, state owner, scaling dimension, or trust boundary changes.

System design derived from the flows

If every service imports concrete clients for every neighbour, the diagram hides compile-time coupling and runtime cascades. Zheta Forge depends on narrow ports, while adapters implement HTTP, queue, database, and cloud details.

Use case IDEntry pointResponsible servicesAuthoritative storeFailure evidence
D03-UC-01Architecture contract validatorOrganization, Application Catalog, Generation Orchestrator, Preview Manager, Connector Broker, Data Provisioner, Release Controller, Audit Evidence, Lifecycle servicesVersioned context-map contractMultiple writers, ownerless record, circular dependency, or undocumented interface
D03-UC-02Runtime Gateway and generated-app APIIdentity service, Runtime Gateway, generated-app authorization adapter, Audit EvidencePlane-local grants and immutable audit eventWrong token audience, missing mapping, revoked grant, or absent denial trace

The provider plane owns organizations, projects, specifications, generation, and release policy. Hosted-runtime controllers own artifacts, deployment, secrets, network, and workload identity; generated services own tenants, users, roles, sessions, and domain data.

Data model and ownership

Cross-plane foreign keys would let one database enforce rules for a plane it does not own. The model therefore uses opaque references across boundaries and local foreign keys only within an owner's store.

Generated-application database: Not created in this slice — the context map and authorization test evidence are sufficient before any generated domain is provisioned.

Record or entityStore and ownerPrimary keyForeign key or opaque referenceTenant keyMaterial constraintLifecycle and deletionUse case IDs
ServiceOwnershipContractGit contract, owned by Platform Architectureservice_name plus contract_versionNone — root architecture recordNone — global platform contractOne writer per entity; dependencies point toward interfaces, not adaptersVersioned with review; retired only after consumers migrateD03-UC-01, D03-UC-02
CrossPlaneAuthorizationEvidenceEvidence S3 bucket, owned by Audit Evidenceevidence_run_idOpaque actor, grant, resource, and trace referencesorganization_idContains positive and negative paths with immutable source IDsAppend-only retention; expires by organization evidence policyD03-UC-01, D03-UC-02

The contract makes service ownership reviewable, while the paired authorization traces prove that the drawn trust boundaries exist in behavior.

Validate the context map

A diagram can drift from code unless it uses the same responsibility names as the implementation. The current Python model defines narrow ports for generation, runtime deployment, artifact storage, and event publication; it does not yet ship the proposed context-map validator or a complete generated-application authorization service. Read the exact model in services/control_plane/domain.py.

class Generator(Protocol):
    def generate(self, blueprint: dict[str, object]) -> dict[str, str]: ...


class Runtime(Protocol):
    def deploy(self, organization_id: str, app_id: str, payload: dict[str, object]) -> dict[str, object]: ...
    def delete(self, organization_id: str, app_id: str) -> None: ...
Declared intentInterpreterSoftware effectHardware effectEvidence
The control-plane model depends on capability ports rather than concrete HTTP clientsPython loads the protocols; the composition root in services/control_plane/app.py injects adaptersDomain rules can call generation and runtime responsibilities without importing HTTP, Kubernetes, AWS, or database clientsAt runtime the injected adapters consume service CPU and network sockets; unit-test fakes consume only the test processpython3 -m unittest tests/test_domain.py proves replaceability and tenant denial; HTTP smoke evidence proves the concrete adapters

Decision rules

Split a service when responsibility, authority, state ownership, failure containment, or scaling changes; keep cohesive behavior together otherwise. In the current code, dependency injection supplies adapters at the edge and inversion of control keeps the model dependent on ports; the three-plane authorization design remains incomplete until each plane has its own enforced grant and denial tests.