3.2 Repository Standards
Consistent repositories are easier to discover, understand, and maintain. Their names and boundaries should communicate what they contain before a team member needs to inspect the code.
3.2.1 Repository Naming¶
Every Moonitoring repository uses lowercase kebab-case and names its kind first, then what it holds:
| Kind | Holds | Examples |
|---|---|---|
system |
One system of the platform, with all its components | moonitoring-system-access, moonitoring-system-farm, moonitoring-system-ai_gateway |
app |
One interface: an application people use, which owns no data and calls the systems' APIs | moonitoring-app-console, moonitoring-app-dashboard (the Organization Dashboard), moonitoring-app-farm_sync (the farm sync client), moonitoring-app-moogrant, moonitoring-app-account, moonitoring-app-news (the News site) |
common |
The libraries shared between repositories, one repository per language | moonitoring-common-rs, moonitoring-common-py |
legacy |
A repository of the design before the platform: an MVP or a tool the platform replaces. New work does not build on it, and it is deleted once what it does runs on the platform | moonitoring-legacy-farm_mvp, moonitoring-legacy-afifarm_client, moonitoring-legacy-grants_mvp |
The name says what a repository is before anyone opens it, and a list sorted by name groups itself: the apps, the libraries, the legacy repositories, then the systems. It names the system or the interface, never the Google Cloud project that hosts it, so it stays true when a system moves to another project.
- A name of several words uses underscores, so that hyphens only separate the segments: the system
ai-gatewaylives inmoonitoring-system-ai_gateway. Host names, API paths and capabilities keep the system's own spelling,ai-gateway. - The repositories that serve the whole company keep their short names:
moonitoring-docs,moonitoring-design,moonitoring-infraandmoonitoring-website.
The company prefix is required
Every repository must begin with moonitoring-, followed by its kind and its name.
3.2.2 Monorepo or Polyrepo¶
A monorepo keeps several related components in one repository; a polyrepo gives each component its own.
Use a monorepo when components change, release and deploy together, share tooling or internal packages, and belong to the same people. Use separate repositories when components have independent lifecycles, different owners or different access requirements. Either way, record the choice and its reason in the project's documentation.
On the platform, each system is one repository and each app is one repository. The components of a system, such as its services, workers and migrations, change together and are released under one tag (see Releases & Versioning), while each component is delivered on its own, one delivery per component (Application Delivery, decided 2026-11-11); systems and apps deploy, version and change on their own, each with one owner. A system's Rust components form one Cargo workspace.
Every repository keeps its parts in a repo/ folder. Each part is a package of its own, laid out as in Package Layout, and its name says what it is:
- In a system repository, a part is named after the system and its role:
<system>-backendfor the service,<system>-consumer,<system>-relayand<system>-sweeperfor its workers,<system>-migratefor its migration job. The crate ofrepo/farm-backendismoonitoring-farm-backend. A system may also ship the client side of its own protocol: a library its clients link (<system>-client) and an app it distributes (<system>-installer), when several interfaces need the same logic;releasesdoes (decided 2026-11-10). - In the other repositories,
app-<name>is something that runs, such as a web site, a desktop application, a server or a tool, andpackage-<name>is a library that other parts import.
The root holds only what spans the parts: the README with what each part is, the AGENTS.md, the workspace manifests and lock files, shared tool configuration, CI and the docker-compose.yml for local development.
moonitoring-system-farm/
README.md what each part is
AGENTS.md
Cargo.toml the workspace of the parts, with Cargo.lock
cloudbuild.yaml
docker-compose.yml
repo/
farm-backend/ the service: README.md, Cargo.toml, src/, tests/
farm-relay/ a worker
farm-migrate/ the migration job
Each part can be built, tested and understood on its own: it keeps its own README, manifest and tests, and the other parts use it through its package name.
3.2.3 Package Layout¶
A repository holds one or more packages: an application with an entry point, or a library. Every package is organized the same way, whatever its language or business, so anyone can find their way in a package they have never opened.
The rules below use three words:
- Must: a required responsibility or dependency rule.
- Default: the usual location. A language's tooling may require another place for the same responsibility; the package README then says where (see Applications and Libraries).
- Optional: create it only when it has real work to do. Empty folders and pass-through classes that only complete the tree are not created.
package/
README.md required: what the package is, its entry points or public API, exact build and test commands
AGENTS.md when coding agents work on the package: boundaries and validation commands
docs/ optional: project documentation, served with `uv run docs`
src/ required: the source code
<entry points> main.rs, lib.rs, __main__.py: start the program or expose the library, nothing else
presentation/ how the outside world talks to the package
view/ screens and components of a graphical interface
controller/ handlers of HTTP requests, messages and commands
domain/ business rules and application operations
entity/ the things the business talks about, immutable, with their rules
type/ values with no identity or lifecycle of their own: value objects, enums, identifiers
repository/ abstract contracts for what the domain needs from storage
usecase/ one operation the application performs per file, such as register_calving_usecase
data/ how data is stored and fetched
model/ records as stored or received, with their conversion to and from domain types
repository/ the implementations of the domain's repository contracts
shared/ optional: code used by more than one feature of the package
utils/ helpers with no business rule, one purpose per file
injection/ builds the concrete pieces and wires them together at start-up
tests/ tests across modules, where the toolchain looks for them
migrations/ optional: the database schema changes this package owns
examples/ optional: how to use a library's public API
benches/ optional: reproducible benchmarks
<build manifests, lock files and tool configuration, where the toolchain expects them>
Application operations have one home, domain/usecase: the data layer stores and fetches, it does not orchestrate. Each use case is one file and one type, and both names end in usecase (decided 2026-11-10): register_calving_usecase.rs holds RegisterCalvingUseCase with its input and output types, and nothing else. Its documentation comment says what the operation does and who is expected to call it. A domain with many use cases groups them in subfolders, such as usecase/groups/create_group_usecase.rs. How the code inside these folders is shaped (domain-driven design, dependency injection, encapsulation) is described in Programming Patterns.
3.2.4 What Each Folder Is For¶
| Location | Responsible for | Must not become |
|---|---|---|
| Entry points | Starting the program, calling injection, choosing which interface runs, shutting down. A library's entry file exposes its public API |
A place for business rules or long workflows |
presentation/view |
Showing output and collecting what the user does | The business model or the place where permissions are decided |
presentation/controller |
Reading an HTTP request, a message or a command, calling a use case, turning its result into a response | A database client or a second copy of the business rules |
domain/entity |
Business concepts with an identity: their valid states, their behavior and their invariants. Entities are immutable: a change returns a new instance | Database rows, HTTP request objects or vendor records; objects changed in place |
domain/type |
Values with no identity or lifecycle of their own: value objects (a weight with its unit, a time range), enums, identifiers. Immutable and compared by value | A place for entities, or for database and API records |
domain/repository |
Abstract contracts only (Rust traits, Python abstract classes): the storage capabilities the domain needs, in the domain's own types and errors | Implementation code, a generic create-read-update-delete interface for every entity, or a door to SQL sessions |
domain/usecase |
One application operation per file, named <operation>_usecase: its preconditions, its permission checks, its rules, and what must happen atomically |
Direct SQL, SDK calls, framework handlers or a hidden service container |
data/model |
Rows, documents or external payloads as they are stored or received, and their conversion to and from domain types, implemented on the model itself: From and TryFrom in Rust, to_domain() and from_domain() in Python |
A model passed unchanged through every layer for convenience, or a hidden home for business decisions |
data/repository |
The implementations of the domain's contracts: queries, file and API calls through the clients that injection passes in, and storage-specific atomic operations |
Application policy or orchestration, or a second public interface that bypasses the domain |
shared |
Code that more than one feature needs, with one clear purpose each: a value type, an error type. Helpers with no business rule go in shared/utils/, one purpose per file, such as shared/utils/pagination.rs (decided 2026-11-10) |
One utils or common file that holds unrelated helpers and grows without end, or a helper that hides a business rule |
injection |
Creating the concrete implementations and configuration, passing them where they are needed, releasing them at shutdown. In a package with features, each feature has its own and the package's one starts them all (see Growing a Package) | A service locator that the rest of the code calls |
A contract and its implementation share a concept and differ in name, for example OrderRepository in the domain and PostgresOrderRepository in the data layer. Dependencies that are not storage, such as a clock or a notification sender, may be declared in an optional domain/port/ instead of being forced into a repository; they are abstract contracts too.
A conversion that can fail, such as a database row with a value the domain does not accept, returns an error instead of building an invalid object. Keep two representations apart only when their meaning, lifecycle or constraints differ. Formatting for a view, converting for a database and validating a business rule are three different jobs.
3.2.5 Dependency Rules¶
The domain must be understandable and testable without starting a user interface, a web framework or a database. Imports enforce this: code depends only inward, on the domain, and reaches concrete implementations at run time through contracts that injection fills in.
| Component | May depend on |
|---|---|
| Domain entities, types and rules | Other domain concepts and shared; never presentation, data or injection |
| Domain repository and port contracts | Domain types and domain-owned errors |
| Domain use cases | Domain concepts and contracts |
| Presentation | Domain use cases and their inputs and outputs |
| Data | Domain contracts and types, other data implementations |
shared |
Only other shared code of the same package |
| Injection and entry points | Every concrete component needed to build the program |
- No production code depends on test code, and no dependency cycles exist.
- A contract never carries an outside dependency into the domain: no ORM object, framework request or driver error in its signature.
- Configuration, time, randomness and external effects are passed in where they matter, never read from global state inside the domain.
A request then flows like this, without the use case ever importing PostgreSQL:
controller -> CreateOrderUseCase -> OrderRepository contract
|
+-> PostgresOrderRepository, supplied by injection
The use case decides what must be atomic; the data layer realizes it. An implementation must not commit each call on its own when the use case needs one transaction.
3.2.6 Applications and Libraries¶
The same model fits packages of every size. Create only the parts a package needs:
| Package | Minimum useful structure |
|---|---|
| Graphical application | Presentation, domain, entry point and injection; data only when it stores or fetches something |
| HTTP service, worker or command-line tool | Controllers, domain, and data and injection as needed. No view/ |
| Pure calculation or transformation library | Public API, domain types and functions, tests. No controller, repository or injection |
| Client or persistence library | Public API, its contracts and types, the concrete integration. No invented business domain |
A library never starts services, opens connections or reads environment configuration when it is imported. Acquiring resources is an explicit call, and the application that uses the library controls its configuration and lifecycle.
Languages keep their own tooling conventions:
- Rust. A Rust system reaches its database through SeaORM: its entities live in
data/model, convert to domain types withFromandTryFrom, and never leave the data layer.src/lib.rsexposes a library;src/main.rsandsrc/bin/define programs. Unit tests stay next to the code in#[cfg(test)]modules; tests across modules go intests/at the package root, where Cargo looks for them. Modules and visibility enforce the boundaries; folder names alone do not. A repository with several packages is a Cargo workspace.typeis a reserved word in Rust, sodomain/type/is declared aspub mod r#type;anddomain/mod.rsre-exports its contents (pub use r#type::*;): the rest of the code writesdomain::Weight, neverr#type. - Python. The layers live under
src/<package>/, with tests intests/besidesrc/, where pytest looks for them. Entry points are declared inpyproject.toml. - Slint interfaces.
.slintfiles belong topresentation/view; their callbacks call use cases and hold no business rules.
3.2.7 Growing a Package¶
A small package has one set of layers directly under src/. A larger package groups code by feature: each feature folder holds its own presentation, domain, data and injection, organized as in Package Layout, while shared and the package's injection stay at the top:
src/
main.rs calls the package injection once, at start-up
orders/ feature
presentation/
controller/
domain/
entity/
type/
repository/
usecase/
data/
model/
repository/
injection/ builds the orders feature
billing/ feature, same layers as orders/
presentation/
domain/
data/
injection/ builds the billing feature
shared/ code used by both orders/ and billing/
injection/ the single entry point that starts every feature
Injection is split the same way, so that each feature wires itself and the package starts them all from one place:
- A feature's
injectionbuilds that feature and nothing else. It receives the shared resources it needs, such as the database pool, configuration and clients, creates the feature's repositories, use cases and controllers, and returns its entry points to the caller. It is the only place that knows the feature's concrete types. - The package's
injectionis the single place where the program starts. It reads the configuration, opens the shared resources once (database pool, Pub/Sub client, telemetry), calls each feature'sinjectionwith them, hands the resulting controllers to the entry point, and releases the resources in reverse order at shutdown. -
A feature's
injectionnever calls another feature's. Whenbillingneeds an operation oforders,billingdeclares a contract for it in its own domain, and the package'sinjectionpasses in the implementation fromorders. -
Feature names come from the business. A feature exposes deliberate contracts and never reaches into another feature's repositories or tables.
- A feature folder does not mean a separate service, deployment or repository.
- Code moves into
sharedwhen a second feature needs it, not in advance, and each piece in it has one purpose. Code shared between repositories goes into the library of its language,moonitoring-common-rsormoonitoring-common-py(see Repository Naming), and holds no business entity: each system keeps its own model. data/contains code, never datasets, database files or credentials. Test fixtures are synthetic or explicitly approved.- Each database has one owner and one sequence of migrations, in
migrations/by default (a system keeps them in its migration part,<system>-migrate). A library never migrates the database of the application that uses it: when it needs a table, it exports the statements that create it and each system's own migration calls them.
3.2.8 Running and Shipping a Repository¶
Every part that runs ships as a container image, built from its own Dockerfile next to its manifest:
- A multi-stage build: dependencies are compiled in a cached layer (
cargo-cheffor Rust), the program in the next, and only the binary is copied into a distroless runtime image. - The program runs as a non-root user and reads its configuration from environment variables, which the deployment sets; the image holds no secret and no environment-specific value.
- One image per runnable part: the API, each worker and the migration job of a system are separate images built from the same commit.
Development runs on the developer's machine with docker compose at the repository root: PostgreSQL 17, the Pub/Sub emulator and a fake Cloud Storage, so that no change needs the cloud to be tried. Tests that touch the database run against a real PostgreSQL in a container, never against a mock of SQL.
Naming and Ports on a Developer's Machine¶
Every repository runs next to every other one, so that a person can start access, farm, releases and the Console at once and the Console calls the real systems (decided 2026-11-05). Docker Desktop then lists them together, named the same way:
- The compose project is the repository's name:
name: moonitoring-system-access,name: moonitoring-app-console. Docker names everything after it: the containersmoonitoring-system-access-backend-1, the images it buildsmoonitoring-system-access-backend, the network and the volumes. The repository's name, not the system's, because a system and an app can share a name, asnewsdoes. - Each service is named by its role, as in the cloud:
backend,consumer,relay,sweeper,migrate,seed,dev-orm,scheduler, andpostgres,pubsub,storagefor what stands in for Google Cloud; an app's service isserver. Lowercase words joined by hyphens. - No
container_nameand noimageon a service that is built: the names Docker gives from the project and the service are the convention. - Each repository has its own block of host ports, from the table below, so that nothing collides. Inside its containers a program keeps its usual port (8080, 5432, 8085, 4443); only the port on the host changes.
| Repository | N | API, app | Dev ORM | PostgreSQL | Pub/Sub emulator | Fake Cloud Storage |
|---|---|---|---|---|---|---|
moonitoring-system-access |
1 | 8100 | 8101 | 5410 | 8185 | 4143 |
moonitoring-system-farm |
2 | 8200 | 8201 | 5420 | 8285 | 4243 |
moonitoring-system-releases |
3 | 8300 | 8301 | 5430 | 8385 | 4343 |
moonitoring-system-ai_gateway |
4 | 8400 | 8401 | 5440 | 8485 | 4443 |
moonitoring-system-moochat |
5 | 8500 | 8501 | 5450 | 8585 | 4543 |
moonitoring-system-grant |
6 | 8600 | 8601 | 5460 | 8685 | 4643 |
moonitoring-system-news |
7 | 8700 | 8701 | 5470 | 8785 | 4743 |
moonitoring-system-ai_model_platform |
8 | 8800 | 8801 | 5480 | 8885 | 4843 |
moonitoring-system-status |
9 | 8900 | 8901 | 5490 | 8985 | 4943 |
moonitoring-app-console |
1 | 9100 | ||||
moonitoring-app-dashboard |
2 | 9200 | ||||
moonitoring-app-account |
3 | 9300 | ||||
moonitoring-app-moogrant |
4 | 9400 | ||||
moonitoring-app-news |
5 | 9500 | ||||
moonitoring-app-status |
6 | 9600 |
A system's ports are 8N00 for its API, 8N01 for its Dev ORM, 54N0 for PostgreSQL, 8N85 for the Pub/Sub emulator and 4N43 for the fake Cloud Storage, with 8N02 to 8N09 for anything else it serves; an app's is 9N00. A new repository takes the next free N and adds its row here first.
References¶
- The Clean Architecture: dependency direction and use-case boundaries.
- Cargo package layout and Rust test organization.