Skip to content

Bounded contexts

Ampora is structured as bounded contexts with strict ownership rules. This page is the rulebook.

The contexts

Ampora.Web                  // Blazor + REST + endpoints
└── consumes ↓
Ampora.Fleet                // inventory, groups, rollouts, governance, federation, gitops
├── consumes ↓
│   Ampora.OpAmp.Core       // protocol, sessions, agent state
│   Ampora.Collector.Config // YAML parser, validator, lint, visualiser model
└── consumes ↓
Ampora.Persistence          // EF DbContext, migrations
Ampora.Contracts            // shared DTOs, value types

Rules

Ampora.OpAmp.Core knows nothing about the Collector

The OpAMP context handles the wire protocol, session state, capability negotiation, and dispatching opaque frames. From its point of view, a configuration is a byte blob with a hash. The OpAMP context has no OtelPipeline, no BatchProcessor, no understanding of "is this a valid Collector config".

Why: this is what lets us manage any OpAMP-capable agent. If the day comes that we ship a non-Collector OpAMP agent, the wire protocol code is not the place that needs to change.

Ampora.Collector.Config knows nothing about OpAMP

The Collector context handles YAML parsing, normalisation, validation, lint, and rendering for the visualiser. It does not know about agents, sessions, capabilities, dispatch, or anything OpAMP-shaped. Its inputs are YAML strings; its outputs are validation results, graph models, and lint findings.

Why: separation of two complex problem spaces. The collector schema evolves on its own cadence (every collector release), the OpAMP protocol evolves on its own cadence. Mixing them would couple changes that have no business being coupled.

Ampora.Fleet is where the two meet

Fleet orchestrates rollouts, governance, GitOps, federation. It is the only context that imports both Ampora.OpAmp.Core and Ampora.Collector.Config. It binds the two together (e.g. "validate the YAML, hash it, dispatch the hashed frame via the OpAMP session").

Fleet itself is split into sub-namespaces (Inventory, Groups, Rollouts, Governance, Federation, GitOps), but they are internally referenceable — the bounded-context boundary is at the project level, not the namespace level inside Fleet.

Ampora.Web consumes application services only

Endpoints and components in Ampora.Web go through application service interfaces (e.g. IRolloutService, IConfigurationService). They do not instantiate repositories directly, talk to the DbContext directly, or build domain entities directly.

This separation is what lets the same service back the UI and the REST API; it also means a UI change cannot accidentally bypass a governance rule.

Ampora.Persistence has no business logic

The persistence context is DbContext + entity configurations + EF migrations. It does not contain validation, governance, or rollout logic.

Ampora.Contracts is the shared vocabulary

Cross-cutting DTOs, value types, and enums that multiple contexts need. Anything in Contracts is part of the public API surface — breaking changes here propagate widely.

Enforcement

A custom Roslyn analyzer in tools/analyzers/ flags illegal imports on every build:

  • Ampora.OpAmp.Core cannot reference Ampora.Collector.Config.
  • Ampora.Collector.Config cannot reference Ampora.OpAmp.Core.
  • Ampora.Persistence cannot reference Ampora.Fleet.
  • Ampora.Web cannot reference Ampora.Persistence directly (other than for DI bootstrap in Program.cs).

CI fails on a violation.

When you think you need to break a rule

Don't. Talk to the maintainers first. There is almost always a way to express the need with an additional service interface, an event, or a DTO; breaking the boundaries makes the next refactor harder for someone else.

Genuine exceptions get an ADR.