Domain-driven design
Execution Core owns the Run aggregate and its child Attempts. Site Coordination owns each configured environment's command stream, with commands and results as children of its stream aggregate. Site Runtime owns Site-local delivery progress, native recovery and member/workspace rules. Its private SQLite journals record local recovery facts; Run/Attempt authority remains in Execution Core and command/result authority remains in Site Coordination. It has no platform PostgreSQL database. Support owns the SupportCustomer aggregate, keyed by Pharos user ID, and the Ticket, TicketMessage and TicketSync aggregates. Choose further bounded contexts from the business language and authority they need, rather than from the three service names. A service is a deployment boundary; a bounded context is a model boundary.
Pipeline Coordination owns the Pipeline aggregate and its ordered child steps. The domain records immutable Workload revision references, execution environments and advancement conditions. Repository references select a branch whose latest contents are fetched when each action prepares. Its coordinator records Run references through owner interfaces; Execution Core retains Run/Attempt authority. Internal start, read and cancellation-request handlers use explicit Project capability and exact Workload revision queries, with current access checked before receipt replay. PostgreSQL commits its aggregate snapshot, command receipt and pending coordinator work together. The coordinator saves stable step launches before submission, follows Runs through owner interfaces and consumes typed completion facts with atomic receipts and progress. Its work leases fence stale workers. Execution Core retains Run creation and retry authority.
Workload Portfolio owns the Workload aggregate and its immutable, numbered revisions. Each revision keeps the execution specification and required inputs that Execution Core plans from. Other contexts read saved revisions through Workload Portfolio's published query interfaces and check member access themselves.
SSH Access owns one MemberSshAccess aggregate per member and Site, with the member's keys as children. Each submission, resubmission or revocation issues a new key generation; Site results are fenced by generation, and applied changes count only after a configured propagation window.
An aggregate owns one immediate consistency boundary. Entities retain identity; value objects compare by value. Children can change only through their aggregate root. Identify the invariant, command/query and owning fact before introducing persistence. Model aggregate boundaries from business consistency and independent lifecycles. If an immediate invariant spans proposed roots, reconsider that boundary. Changes to separate children still compete for their root's version.
Workflows spanning aggregates have explicit allowed pending states and an owner for each recovery step. For example, an accepted launch awaiting Run creation is a valid pending state when its intent is durable and recoverable. Describe the conditions for progress, retry and terminal outcomes in the owning context.
Commands use explicit canonical identity and expected versions. A retry with the same material input returns its recorded outcome. Reusing a command identity for different input is a conflict. Expected business rejection is typed and recorded; infrastructure outages and corrupt restored authority remain retryable failures. Duplicate or no-op transitions emit no new domain events. Keep transport deduplication separate from business uniqueness: each consumer has its own receipt, while a stable business key protects the same occurrence arriving under different message identities. Correlation IDs group related work; deduplication uses command/message identities and explicit business keys.
Queries use named inputs and handlers, application-owned read models and separate read repository ports. They have no authoritative side effects. Owner adaptors decode stored or provider data and map it into those read models. Preserve root revisions when reading versioned authority, and make missing-resource behaviour explicit. Pagination is explicit: a query must return the complete collection or a traversable cursor. Domain code has no framework, transport, database, provider, implicit clock or random-number dependency. Time and identities arrive as explicit values.
A domain event is a context-local fact. Application mapping decides whether it needs private durable delivery or a public versioned integration contract. Required reactions across aggregates happen asynchronously through the same receipt/outbox mechanism. Do not invoke a second aggregate's command handler inside the first command transaction.
Learn facts from another context through its published integration events or query interfaces. Maintain consumer-owned projections where local reads are needed. Between services, use the versioned Protobuf contracts and messaging conventions defined in ADR 0046. Database co-location permits no foreign SQL access. External IDs remain opaque provider bindings; canonical Pharos identity is independent of them.
Model ownership does not require local persistence. A supporting context can translate an authoritative third-party model through its own ports and adaptors without owning a database. Whenever it persists state, including delivery receipts or projections, that storage belongs exclusively to the context under the persistence rules.
Go and Python use idiomatic native types while preserving the same consistency, rejection and delivery rules. Generated wire types remain in outer adaptor or contract packages. Provider-independent application DTOs remain context-owned. Static types describe valid shapes; boundary decoders must also validate external input and restored state at runtime. Malformed requests follow the boundary's validation policy. Corrupt stored authority remains a retryable state failure, distinct from an expected business rejection. Keep provider, transport and snapshot mappings in the owning context's adaptors; shared infrastructure carries technical mechanisms.
Use British English and adaptor. Name domain/application modules after their
context, adaptors after their capability/technology, and executable composition
roots after the process they run. Classify driving/driven relative to the port.
See ARCHITECTURE.md for the repository map and docs/adding-a-context.md for registration steps.
Themelio's Python scaffold uses a central command executor around plain handlers and named write repository ports. An aggregate save stages private domain facts. An event reaction persists its receiving receipt and exact outgoing command bytes; a separate command consumer executes that command. Publication handlers map private facts into integration events. Realtime intent is explicit and commits with its owner transaction. These mechanisms introduce no Themelio business model.
Page source
Source snapshot · Revision f928834c8.