Skip to content

Seven decisions Olly has standardised so every service compounds rather than fragments.

Updated Aug 11, 2026

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/events is 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 the roles-poc partial-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.

Olly Health Insurance Platform