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 ID | Actor | Customer job | Success outcome | Denial or recovery evidence |
|---|---|---|---|---|
| D03-UC-01 | Platform architect | Assign each create-to-retire action to one service and store owner | No record has two writers and every cross-service reference is explicit | Duplicate writer or circular ownership fails the architecture check |
| D03-UC-02 | Security architect | Keep provider, hosted-runtime, and generated-app grants separate | Each plane authenticates, authorizes, audits, and revokes independently | A 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 ID | Use case IDs | User story | Observable acceptance conditions |
|---|---|---|---|
| D03-US-01 | D03-UC-01 | As a platform architect, I want one writer for every durable record, so that retries and failures cannot create competing truth | Context-map validation reports one service/store owner and explicit opaque references for every entity |
| D03-US-02 | D03-UC-02 | As a security architect, I want tokens scoped to one authorization plane, so that control-plane membership cannot bypass app roles | Cross-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 ID | Use case IDs | Path | Trigger | Numbered steps | Terminal evidence |
|---|---|---|---|---|---|
| D03-FLOW-01 | D03-UC-01, D03-UC-02 | Happy | Architect submits the Zheta Forge context map | 1. 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-02 | D03-UC-01, D03-UC-02 | Denied | Control-plane credential calls a generated-app domain endpoint | 1. 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 ID | Entry point | Responsible services | Authoritative store | Failure evidence |
|---|---|---|---|---|
| D03-UC-01 | Architecture contract validator | Organization, Application Catalog, Generation Orchestrator, Preview Manager, Connector Broker, Data Provisioner, Release Controller, Audit Evidence, Lifecycle services | Versioned context-map contract | Multiple writers, ownerless record, circular dependency, or undocumented interface |
| D03-UC-02 | Runtime Gateway and generated-app API | Identity service, Runtime Gateway, generated-app authorization adapter, Audit Evidence | Plane-local grants and immutable audit event | Wrong 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 entity | Store and owner | Primary key | Foreign key or opaque reference | Tenant key | Material constraint | Lifecycle and deletion | Use case IDs |
|---|---|---|---|---|---|---|---|
| ServiceOwnershipContract | Git contract, owned by Platform Architecture | service_name plus contract_version | None — root architecture record | None — global platform contract | One writer per entity; dependencies point toward interfaces, not adapters | Versioned with review; retired only after consumers migrate | D03-UC-01, D03-UC-02 |
| CrossPlaneAuthorizationEvidence | Evidence S3 bucket, owned by Audit Evidence | evidence_run_id | Opaque actor, grant, resource, and trace references | organization_id | Contains positive and negative paths with immutable source IDs | Append-only retention; expires by organization evidence policy | D03-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 intent | Interpreter | Software effect | Hardware effect | Evidence |
|---|---|---|---|---|
| The control-plane model depends on capability ports rather than concrete HTTP clients | Python loads the protocols; the composition root in services/control_plane/app.py injects adapters | Domain rules can call generation and runtime responsibilities without importing HTTP, Kubernetes, AWS, or database clients | At runtime the injected adapters consume service CPU and network sockets; unit-test fakes consume only the test process | python3 -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.