Query Delegation Pattern
This document explains ApexEloquent's design philosophy of separating query "construction" from query "execution". Starting from the long-term operational challenges of the Selector Pattern, widely adopted in Salesforce development, the article lays out why the Query Delegation Pattern resolves them, and how ApexEloquent embodies it.
Introduction
In Salesforce development, the Selector Pattern has long been a popular way to organize data access. Concentrating queries in one place keeps raw SOQL out of business logic and lets the consumer side fetch data through method calls — a clear, simple win.
But run a Selector Pattern long enough on a mid-to-large project, and the request "I want a slightly different query" comes up over and over. Methods and arguments accumulate as patches. The Selector class swells, and the original simplicity erodes bit by bit. On top of that, since tests themselves require a database, you're stuck writing only integration tests.
What Happens to a Selector Pattern Over Time
- Each new use case wants a slightly different query, and methods / arguments keep getting tacked on — the Selector bloats
- Splitting Selectors per use case generates a swarm of look-alike Selector classes and interfaces
- Flag arguments and conditional branches pile up; method readability and maintainability drop
- A query change made for one use case quietly affects another
- Tests skew toward "implement mock classes for Selectors" and DB-backed integration tests
Why Separation Started to Feel Necessary
While dealing with this kind of erosion, one question arrived:
"Are query construction and query execution really the same responsibility?"
Deciding which object to query, and with what conditions, is part of the business logic — deeply rooted in the domain's rules and use cases. Actually issuing the assembled query against the DB and returning the result, by contrast, is pure I/O.
Rather than have these two coexist inside a Selector, the idea was to draw a clear line between them. The thinking that followed landed on what we now call the Query Delegation Pattern.
Problems with Traditional Patterns
What Goes Wrong When Construction and I/O Mix
A typical Selector holds both responsibilities — "what data, and how to retrieve it" (query construction) and "actually hit the DB and return the result" (execution). Construction lives close to business logic; execution belongs to the infrastructure layer. Sorted that way, the two are fundamentally distinct responsibilities.
When they mix inside a Selector, the situation degrades over time:
- Query assembly is locked inside the Selector, and from the domain side it becomes hard to see "what are we going after, and how"
- Modifying a query risks rippling into every feature that uses that Selector
- Tests need mock implementations against the Selector interface, and preparing per-test variations becomes overhead
- When one Selector concentrates the logic, the operational pattern devolves into "keep adding methods", dragging down continuous feature development
- Splitting Selectors per use case produces rows of look-alike interfaces and implementation classes, increasing the psychological cost of writing tests
Ideally, "what data do I need" should be expressed explicitly on the use-case (domain) side, and the Selector should be responsible only for "execution". That's the starting point of the Query Delegation Pattern.
The Cost of Reusability: Lost Context
One attraction of the Selector Pattern is reusability. A single method that "filters records under a specific condition" can be shared across multiple use cases.
But this reusability is often traded for blurred business intent. A Selector method designed for general reuse tends to lose the information "why is this data needed?" and "in what context is it used?".
💭 The more general-purpose a Selector method becomes, the further it drifts from concrete business intent.
This matters for testing too. When a method's business intent is vague, writing test cases gets vague along with it — "what exactly should I verify?" — and as a result, tests end up thin.
What Is the Query Delegation Pattern?
The Basic Concept
The Query Delegation Pattern is the approach of clearly separating the responsibilities of "query construction" and "query execution".
| Responsibility | Owner | Nature |
|---|---|---|
| Query Construction | The domain side (Usecase) | Assembles conditions based on business rules |
| Query Execution | ApexEloquent's built-in IEloquent | Receives the assembled query and handles I/O with the DB |
Where the traditional Selector held both responsibilities, the Query Delegation Pattern separates them:
- The domain side (Usecase) builds the blueprint of the query
- The execution side (
IEloquent) receives the blueprint and runs it
💡 The execution side stops caring about "what to fetch" and cares only about "how to fetch".
Role Division Inside ApexEloquent
ApexEloquent realizes this separation at the framework level with three core pieces.
| Role | Class | Description |
|---|---|---|
| Query Builder | Scribe | An immutable builder that assembles the query blueprint via type hints and method chains |
| Data Access | IEloquent (Eloquent / MockEloquent) | The side that takes a blueprint and issues SOQL. Production uses Eloquent, tests use MockEloquent, swapped via DI |
| Record Wrapper | IEntry (Entry / MockEntry) | A wrapper around fetched results. Handles SObject and AggregateResult through one interface |
Query Reusability
How to Handle Reusable Queries
The default stance of the Query Delegation Pattern is to assemble queries individually per use case — that keeps business intent in the code. But the practical reality is that "I want this query shared across multiple use cases" does come up.
ApexEloquent answers this with two options. Consider team size, domain complexity, and how much commonality the queries have, and consciously pick which way the project leans.
Option 1: Keep It Inside the Usecase
Build the Scribe right next to the business logic and preserve context completely. The Handler-Usecase Architecture of Apex Stem takes this as the default.
Benefits:
- The context "why this data is needed" stays in the code
- Unexpected impact on other use cases is unlikely
- Reading the use case alone gives you the full picture of what data it needs
Option 2: A "Query Vault" — a Selector Derivative
Extract the Scribes you want to share into a utility-like class and have each use case explicitly opt in. This is a Selector-Pattern-derived style that offers "query parts".
Benefits:
- Heavily duplicated queries consolidate in one place
- Since the shared query is passed as a
Scribeobject, the use case can append further conditions with chained methods - A gradual migration from the Selector Pattern flows naturally
Caller-side feel:
// 1. Pull the base "filter by Id" query from the vault
Scribe oppScribe = OpportunityVault.getById(oppId);
// 2. Append a use-case-specific condition
oppScribe = OpportunityVault.addNameCondition(oppScribe, '%TestName%');
// 3. Add quotes as a child subquery
List<String> quoteFields = new List<String>{ 'Id', 'Name', 'GrandTotal' };
oppScribe = OpportunityVault.addQuotes(oppScribe, quoteFields);
// 4. Delegate execution to IEloquent (Query Delegation)
List<IEntry> entries = this.eloquent.get(oppScribe);
We use the word Vault here for the Opportunity query vault.
Each vault method just returns a Scribe; SOQL issuance is concentrated in the final IEloquent.get(scribe) call. That's the mechanism that makes the Query Delegation Pattern and the "vault" style coexist. With a traditional Selector — "the method runs SOQL inside and returns the result" — there's no room for the use case to append conditions, so the only escape is to keep adding methods or argument variations.
Why Both Options Work
In either case, what makes both options viable is that Scribe — through .field() / .whereEqual() and friends — has the property of assembling queries as parts via method chains. Building from scratch inside a use case, or grabbing a mid-state Scribe from a vault and tacking on more .whereEqual(...), feels exactly the same.
🎯 It's not "one option is correct". Pick what fits your team's situation; you can also migrate from one to the other later.
Comparison with Traditional Patterns
What Changes
| Aspect | Traditional Selector | Query Delegation Pattern |
|---|---|---|
| Responsibility placement | Query assembly and DB execution live together | The domain assembles the query; IEloquent only executes |
| Long-term operation | Patches accumulate; bloats; loses simplicity | Structure stays stable; easy to add features incrementally |
| Test style | DB-backed integration tests dominate | Swap in MockEloquent and write DB-less unit tests |
| Visibility of intent | Generalized methods lose context | Assembly lives right next to the use case — why this data matters stays |
Implementation in ApexEloquent
ApexEloquent embodies the Query Delegation Pattern at the framework level. The key implementation features:
Dynamic Query Construction
The domain side builds with Scribe and appends conditions to fit the context. Scribe is immutable, so deriving multiple queries from a shared Scribe ("last month's" / "this year's") doesn't have them step on each other.
Query Execution Comes Built-In
The execution side is handled commonly by ApexEloquent's built-in IEloquent. You don't have to mass-produce Selectors yourself — handing over a built Scribe issues SOQL.
Mock Swap for DB-less Unit Tests
Because construction and execution are split, swapping IEloquent for MockEloquent is enough to write DB-less unit tests. MockEloquent also exposes Spy properties like upsertedRecords / deletedCount, so you can assert what was DML'd.
Mocking Non-Writable Fields
Formula fields, rollups, parent relationships, auto-number — fields you can't normally write to — are freely assignable on the MockEntry side. Values that production can only obtain through computation can be handled in tests as "verify the logic on the premise that this value is returned".
Where to Start with the Implementation
The entry path is to first read Building Queries with Scribe, then Data Access, DML, IEntry, and Mock — and how Query Delegation lands in real code becomes immediately visible.
Summary
What the Query Delegation Pattern Resolves
The Query Delegation Pattern is the approach of root-cutting the responsibility blur — "query construction" and "DB execution" — that Selectors tend to carry.
Main effects:
- Long-term maintainability through clear responsibility separation
- Easier test construction without writing complex mock implementations
- Healthier domain models through visible query intent
- Lower coupling between business logic and the data-access layer
The Query Delegation Pattern isn't just a technical trick — it's a philosophy that pushes how you think about data-access-layer design in a cleaner direction.
Related Documents
- ApexEloquent: the full picture of the OSS that adopts the Query Delegation Pattern
- Apex Stem: the combination of the 4 OSS including ApexEloquent and the Handler-Usecase Architecture
- The Repository Pattern in Apex: Trial, Error, and Going Built-In: why the Repository was built into ApexEloquent (and how it relates to the Selector Pattern)
- From Raw SOQL to a Chained-Method ORM in Apex: a migration primer from raw SOQL to Scribe
- MockEntry: How Apex Test Data Construction Becomes Possible: the mock-side Deep Dive
- Catching Mock-Test False Positives: A Safety Net for SELECT Omissions: catching SELECT omissions at unit-test time via coordination with Scribe
- Handler-Usecase Architecture: where Query Delegation lands in real code (the Usecase layer)
- ApexEloquent Guide: how to use Scribe / IEloquent / IEntry, plus the API reference