Design Principles
Olly is built as many small services with a shared spine. Decisions that touch how those services interact - data ownership, identity, integration, change - are settled once and applied everywhere. The seven principles below are the ones that pay back across every team and every feature; they are non-negotiable in code review and surface explicitly in the architecture artefacts.
Position
Olly chooses uniformity over local optimisation: every service owns its data, every mutation is idempotent, every audience is default-deny, every identifier is a typed locator. The cost of these constraints is small per-service; the compound benefit is that adding a new service, a new audience, or a new partner integration never requires re-litigating the basics.
1. Per-service ownership
A service owns its persistent data and is the only writer to it. Other services reference its entities by locator ID (an externally-meaningful identifier) and stay informed by subscribing to its events, never by reading its database.
- Why: keeps schema evolution local to one team. Removes the "shared database becomes the API" anti-pattern that turns every column rename into a cross-team migration.
- In Olly: every Go service has its own Postgres database. Cross-service consistency lives in Kafka topics + an outbox per writer. No service joins another service's tables; no shared mutable state.
2. Event-first integration
When two services need to coordinate state, they do so through explicit, versioned Kafka events. Synchronous APIs are reserved for user-facing read/write paths where the caller is waiting on the answer.
- Why: synchronous coupling is the default failure mode of microservice estates. Events decouple deploy cadence, absorb back-pressure, and give us a replayable audit trail.
- In Olly: the event catalog at
/data/eventsis authoritative. New services emit at least one domain event for every state change worth knowing about; consumers handle them at-least-once and idempotently.
3. Idempotency by default
Every mutating endpoint accepts an Idempotency-Key header (HTTP) or an idempotency token (Kafka consumer). Retries, replays, and at-least-once delivery are safe by construction, not by hope.
- Why: distributed systems retry - networks blip, sagas redrive, queues redeliver. If idempotency is opt-in, eventually someone forgets and double-charges a member.
- In Olly: the boundary service issues idempotency tokens at the gateway edge. Every Go service writes them to a deduplication table before applying side effects. Kafka consumers commit offsets only after the idempotency record is durable.
4. Two-directional compatibility
Schemas, APIs, and events evolve so old and new clients can coexist. Breaking changes are two-stage: ship the additive change, wait until every consumer has caught up, then remove the old. No database column is ever renamed in a single deploy.
- Why: forward-compatible deploys keep the rollback button armed. Backward-compatible schemas keep services independent. The two together mean we never need a coordinated big-bang.
- In Olly: Protobuf for inter-service contracts; Avro for Kafka events. Required fields are added only via the additive route. Deprecation has a calendar deadline that's tracked in the design doc, not in folklore.
5. Default-deny everywhere
A new audience, a new role, a new permission, a new MCP tool, a new feature flag - none of these silently grant access. The default is nothing visible until the owner explicitly opts something in.
- Why: the alternative (default-allow) means an undeclared field becomes a vulnerability the moment a new audience is added. We've already seen this principle proved by example: when the EY audience was first added, an early version of the audience filter inverted the gate ("strip on public, keep everywhere else"), which silently leaked internal-marked content into the EY build. The inverted gate is now default-deny.
- In Olly: audience-array filtering on docs (this page is opted-in to
[public, internal, ey]); per-tool authz on the MCP server; locator-prefix isolation in Keycloak claims; application-code checks that scope every query by the caller's locator claims. 🚧 Target-state - not yet built: ABAC checks at every PEP and OPA partial-eval over Rego policies. There is no OPA sidecar deployed today (only theroles-pocpartial-eval spike); authorization is enforced in application code, not by a policy engine. See Data Protection and Designs → Access Control.
6. Fail-soft and observable
Degraded states are designed, not accidental. LLM calls fall through to a human; downstream timeouts return cached or partial answers; partner-API outages surface as a clearly-marked degraded experience, never a 500. Every degradation is traced and surfaces in the dashboards on status.dev.hiolly.com.
- Why: systems with no designed degradation fail loudly the first time a dependency wobbles. Olly's triage path can't 500 just because the disposition model is slow.
- In Olly: every service emits OpenTelemetry; LLM traces flow to Langfuse; synthetic monitors in Gatus catch regressions before users do; feature flags via GrowthBook let us turn a degraded path off without a deploy.
7. Locators over UUIDs
Externally-facing identifiers carry semantic prefixes: PRV- for providers, SCH- for schemes, CLM- for claims, PRT- for parties. UUIDs stay internal to the service that owns them.
- Why: locators make support conversations crisper ("can you check CLM-2026-001847?"). They keep logs scannable. They surface ownership at a glance. UUIDs achieve none of this and are indistinguishable from each other when something goes wrong at 2am.
- In Olly: the boundary service issues every locator from a per-namespace sequencer. Service code never accepts a UUID over the wire; the API gateway rejects requests that omit the locator prefix.
What we explicitly do NOT do
- Shared databases between services. Even when "just reading" is tempting, the read becomes a coupling. We pay the cost of an event subscription instead.
- Globally unique surrogate keys as the external contract. UUIDs leak out only through MCP debug tooling, never through user-facing surfaces or partner APIs.
- Synchronous fan-out for write paths. A claim submission does not call seven downstream services in serial; it writes to its own database + outbox and trusts the bus.
- Backwards-incompatible deploys without a two-stage migration. No matter how convenient, the rollback button has to stay armed.
- Optional idempotency. The Idempotency-Key header is required on every mutating route at the gateway; missing-or-malformed is a 400, not a soft warning.
- Implicit audience visibility. A page, a sidebar entry, an MCP tool, a Kafka topic ACL - none of these inherit visibility from a "default everyone" set. Every grant is explicit and reviewed.
