Approach

How we design and build software

These are the engineering patterns we use, with examples from production systems and an explanation of the trade-offs. Our founder has applied them across C#, Kotlin, Rust, Go and TypeScript over twenty-five years.

Code structure

Structure, types and module boundaries

These four patterns guide how we structure a codebase. They apply across languages and are easier to introduce at the start of a project than to retrofit later.

Errors as values

railway-oriented programming, after Scott Wlaschin

Return expected failures as typed values so the caller can handle them explicitly.

Services return typed results for failures that callers need to handle. The database layer translates constraint violations into typed errors, and the transport layer maps those errors to HTTP status codes.

In other stacksRust has Result in its standard library. Kotlin can use sealed hierarchies, C# result types or discriminated unions, Go error returns, and TypeScript tagged unions.

What it costsThis requires explicit mapping code between layers. Transaction rollbacks still use exceptions.

Functional core, imperative shell

Gary Bernhardt; ports and adapters, after Alistair Cockburn

Keep business logic independent of frameworks and external systems.

Reynt’s domain packages have no framework imports. Its HTTP framework migration took a week of changes to route files, leaving domain logic unchanged. Separate payment adapters also allowed us to remove providers without changing settlement logic.

In other stacksIn .NET, a Clean Architecture domain project can have no web-framework references, with handlers in a separate project. Hexagonal architecture on the JVM follows the same dependency direction.

What it costsFramework features such as dependency-injection decorators and request-context plugins must stay in the transport layer. This can require extra adapters or mapping code.

Make illegal states unrepresentable

Yaron Minsky

Use types and constructors to prevent invalid operations.

The tenant, account and authenticated actor are resolved at the request boundary and passed in when the service is created. Service methods use that scope throughout, without requiring callers to supply it again.

In other stacksKotlin supports sealed interfaces and exhaustive `when` expressions; Rust has enums with payloads; C# has records and closed hierarchies; TypeScript has discriminated unions. Constructor validation can cover constraints the type system cannot express.

What it costsMore types and constructors to write, and a state the business allows needs its own type before it can be expressed at all.

Parse, don’t validate

Alexis King

Parse untrusted input at the boundary and pass validated, typed values into the application.

Entry points parse input against schemas. Configuration is validated at startup, so missing variables are caught before the application serves requests. Derived schemas keep configuration definitions in one place.

In other stacksTools include Zod in TypeScript, serde in Rust, model binding and validation in .NET, and kotlinx.serialization on the JVM. The parsed value should carry the guarantees needed by the code that receives it.

What it costsA schema is a second description of each input and has to change with it. One that is too strict turns away requests a lenient check would have let through.

Automated checks

Automated checks for architecture rules

We enforce architecture decisions with tests, database policies, constraints and triggers. Each check runs where it can reliably catch a violation, and becomes part of the codebase alongside the rule it protects.

Architectural fitness functions

Neal Ford, Building Evolutionary Architectures

Test architecture boundaries in CI so violations are caught during development.

Our tests check for framework imports in the domain, missing FORCE ROW LEVEL SECURITY policies, extra writers to the append-only ledger, heavy eager imports and test doubles that no longer match their clients. The RLS scan accounts for table renames.

In other stacksArchUnit on the JVM and NetArchTest or ArchUnitNET in .NET provide dedicated libraries. In other stacks, tests can inspect source files, schemas or manifests and run the same checks in CI.

What it costsThese checks add a small amount to the test suite. Each new architecture rule needs a corresponding check.

Put the invariant where it cannot be routed around

defence in depth

Enforce tenant isolation in both the application and the database.

Services scope access to the current tenant. PostgreSQL row-level security enforces that scope again for each request; access beyond it requires an explicit system scope. The database policy provides protection even if an application check is missed.

In other stacksThe appropriate layer depends on the rule: database constraints for stored data, authentication at a gateway, or filesystem permissions for file access. Place the check where all relevant operations must pass through it.

What it costsEvery query runs under a policy, which costs some planning time, and a job that legitimately spans tenants has to ask for the system scope explicitly.

Crypto-shredding and blind indexes

Encrypt personal data with removable keys while supporting exact-match searches.

Personal data is encrypted with a key associated with the data subject. The master key is managed through a replaceable provider. Keyed blind indexes support lookups on encrypted columns. Destroying the relevant key makes the encrypted data unreadable, including copies in backups.

In other stacksThe parts exist in every stack: an AEAD cipher and HMAC from the platform’s crypto library (libsodium, .NET’s System.Security.Cryptography, the JDK, Go’s crypto packages) and a key store behind a provider interface. The pattern lives in the schema, not the language.

What it costsBlind indexes support exact matches. Fuzzy search needs a separate design.

Append-only ledger, single writer

Preserve the history of charges, permissions and automated actions in an append-only log.

Ledger tables reject SQL updates, and a static test ensures that only one service can insert entries. Current state is derived from that sequence. Integrations that produce the events can then be replaced without rewriting the history.

In other stacksEvent stores such as EventStoreDB, or Marten in .NET, formalise the same idea. In any stack, a table without UPDATE grants and a single service allowed to insert gets most of the way.

What it costsReads become more expensive. Corrections require new entries, so both the original record and the correction remain visible.

Integrations and infrastructure

Working with external systems

We isolate vendor-specific models from the domain, handle external failures explicitly and keep infrastructure manageable.

Anti-corruption layer

Eric Evans, Domain-Driven Design

Translate vendor-specific data models at the integration boundary.

MUSES integrates five external SaaS products behind a single REST API with a shared vocabulary. In Reynt, payment adapters perform the same translation for providers. This allowed two providers to be removed without changing the payment domain.

In other stacksThe layer looks the same in every language: one module per vendor that owns its data types and returns domain types. Generated clients (OpenAPI, gRPC) keep the vendor side cheap to update.

What it costsEvery vendor field the domain needs is mapped by hand, and a vendor change means touching its adapter even when the domain stays the same.

Store-and-forward, backoff with jitter

the transactional outbox pattern

Persist requests before calling an external service so they can be retried after an outage.

In MUSES, the ticket service stores incoming requests before calling the external ticket system, then retries failed calls with backoff. The certificate solver also uses timeouts and retries with jitter, and serialises concurrent challenges.

In other stacksThe outbox comes as a library in most stacks — MassTransit or NServiceBus in .NET, Spring Modulith on the JVM — or as a table and a worker anywhere.

What it costsThe caller gets an acknowledgement instead of an answer, and a retried request can arrive twice, so the receiving side has to be idempotent.

Boring infrastructure

choose boring technology, after Dan McKinley

Keep infrastructure dependencies to what the system needs.

Our workflow engine uses Postgres for queues and timers. Elsewhere we use a single database engine, cron for jobs that tolerate a minute of latency, and containers deployed from manifests. This site is static, with one script for cookieless analytics.

In other stacksPostgres-backed queues exist across stacks: pg-boss and graphile-worker in Node, River in Go, Oban in Elixir, Hangfire with a Postgres store in .NET.

What it costsSimple infrastructure still has limits. A Postgres queue that works at thousands of jobs a minute may need replacing at millions. We monitor those limits as usage grows.

Programs as values

declarative over imperative

Represent workflows as data so they can be inspected before execution.

octaflow represents a workflow as a DAG that can be inspected, tested and displayed before execution. The graph declares idempotency keys, deadlines, retries and compensating steps.

In other stacksThe same split runs through the field: code-first engines such as Temporal on one side, declared graphs such as AWS Step Functions or Argo Workflows on the other.

What it costsA declared graph cannot express arbitrary control flow. An imperative engine may be a better fit when the workflow’s structure cannot be known in advance.

GitOps and a signed supply chain

Define infrastructure in version control and sign released container images.

We use Flux and Terraform for declarative infrastructure, with encrypted secrets in Git. Deployments select container image tags produced by CI. Published containers run as non-root on distroless images with read-only filesystems. Images are signed, include SBOM and provenance attestations, and are scanned weekly for vulnerabilities.

In other stacksArgo CD does what Flux does. Sigstore’s cosign, Syft and Trivy sign, inventory and scan images whatever language built them.

What it costsEven a one-line configuration fix goes through a commit and a reconcile loop, and secrets need their own encryption set up before they can live in Git.

Documentation

Documentation for maintenance and decisions

We record decisions and limitations so that future maintainers can understand why the system works this way.

Negative documentation

Every project says when to use something else.

Each open-source project includes guidance on when to choose another tool and a comparison of the alternatives. This helps users assess its limitations before adopting it.

In other stacksThe section works in any ecosystem: npm, crates.io, NuGet and Maven Central all show the README before anyone installs.

What it costsIt sends some readers to another tool, and it has to be kept current as the alternatives change.

Intent-revealing commits and a work ledger

Explain the purpose and effect of a change in its commit message.

Commit subjects describe behaviour, such as "handle a model refusal without failing the request". Proposals and decisions are kept in a documents folder, moving from open to done as work is completed.

In other stacksArchitecture decision records, after Michael Nygard, and Conventional Commits are widespread forms of the same habit.

What it costsWriting why takes longer than writing what, and the documents folder needs pruning or it becomes a second, stale wiki.

Encoded procedures

Turn repeated development tasks into reusable procedures.

We use executable playbooks for recurring tasks such as CRUD routes, payment providers, data backfills and integration tests. People and agents follow the same procedures, with architecture tests checking the result.

In other stacksRunbooks, Makefiles and project templates are the older forms. Agent skills add a reader that follows them literally.

What it costsA playbook goes stale when the code it describes changes. It needs the same review as code, or an agent follows an outdated recipe to the letter.

Putting it into practice

Types, tests, database policies and reusable procedures help us keep these decisions consistent as the code changes. They also support the review of AI-generated code by catching errors that are easy to miss by reading alone.

Want this applied to your system?

An architecture review can help you decide which of these patterns would be useful in your codebase.