Back to blog

Persistence ignorance in practice: one contract, many stores

Your domain should not know whether data lives in SQL Server, MongoDB, or a Dapper query. Here is how repository contracts and architecture tests let you swap the store without touching business logic.

Engineering
May 19, 2026 9 min read
Persistence ignorance in practice: one contract, many stores

The database you choose on day one is rarely the database you want on day one thousand. Read patterns change, scale arrives, a vendor relationship shifts. The teams that survive those changes cheaply are the ones whose domain logic never knew which database it was talking to in the first place. That property has a name - persistence ignorance - and it is achievable with a small amount of discipline.

The domain depends on a contract, not a database

The core idea is an inversion: instead of business logic calling a database, it calls an interface that the domain itself defines. The interface speaks the domain's language - aggregates and intentions - never SQL or collections:

CSHARP
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct);
    Task AddAsync(Order order, CancellationToken ct);
    Task<IReadOnlyList<Order>> FindAsync(OrderSpecification spec, CancellationToken ct);
}

The domain layer ships this interface and depends on nothing else for data. Where the orders actually live is somebody else's problem - specifically, the infrastructure layer's.

Implementations live at the edge

Each storage technology gets its own implementation in infrastructure, behind the same contract. A Dapper implementation writes hand-tuned SQL for read-heavy paths; an ORM implementation handles rich aggregate mapping; a document implementation targets MongoDB. The domain cannot tell them apart because it only ever sees IOrderRepository:

CSHARP
public sealed class DapperOrderRepository : IOrderRepository
{
    public async Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct)
    {
        const string sql = "select * from orders where id = @id";
        // map rows to the Order aggregate
    }
}

Swapping stores - or running two side by side during a migration - becomes a dependency-injection change, not a rewrite of business rules.

Specifications keep queries out of the domain

The place persistence ignorance usually leaks is querying. The moment a service builds a LINQ-to-EF expression, it is coupled to EF. A specification object - a declarative description of "which orders" - keeps that intent in the domain while letting each repository translate it to its own dialect. The domain says what it wants; infrastructure decides how to fetch it.

Tip: The leak to watch for is IQueryable escaping the repository. The moment a caller composes a query against IQueryable, your provider's quirks become your domain's problem and the abstraction is gone.

Enforce the boundary with architecture tests

Discipline that relies on memory erodes. We back persistence ignorance with architecture tests that fail the build if the domain or application layers reference a persistence package. The rule is mechanical and unambiguous: domain code that imports a database driver is a bug, and CI says so before the bad dependency spreads. The contract is only as strong as the thing that enforces it.

When to keep it simple

Like any abstraction, this one has a cost, and it is not free for a tiny app that will only ever use one database. But the moment you anticipate scale, a migration, multiple read models, or the need to test domain logic without a database, persistence ignorance pays for itself many times over - and adding it later is far more expensive than designing for it now.

Wrapping up

Persistence ignorance comes down to three habits: define data access as a domain-owned contract, put every storage implementation behind that contract in infrastructure, and use specifications plus architecture tests to stop database concerns from leaking inward. Do that and your most disruptive infrastructure change - swapping the database - becomes one of your least disruptive code changes.

Weighing a data-layer decision or migration? Let's talk architecture.

Keep reading

The four pillars of ConnectSoft: Factory, Platforms, Squads, and Marketplace
Company News
June 20, 2026 8 min read

The four pillars of ConnectSoft: Factory, Platforms, Squads, and Marketplace

ConnectSoft is more than a services company. Here is how four reinforcing lines - the AI Factory, live platforms, AI squads, and a marketplace - fit together into one ecosystem.

Read more
The ConnectSoft tech landscape: the stack behind our delivery
Company News
June 18, 2026 8 min read

The ConnectSoft tech landscape: the stack behind our delivery

A tour of the technologies we lean on across delivery - .NET and Blazor, Azure, messaging, data, observability, and AI - and the principles that decide what makes the cut.

Read more
Why we standardized on the Microsoft Agent Framework
Engineering
June 14, 2026 9 min read

Why we standardized on the Microsoft Agent Framework

Choosing the runtime for an autonomous agent mesh is a long-lived bet. Here is the reasoning behind picking the Microsoft Agent Framework, with MCP tools and a model abstraction at its core.

Read more

Subscribe to our newsletter

Get the latest tech insights straight to your inbox

Ready to work with us?

Let's discuss how our team can help achieve your digital goals