Layered Constructor Pattern
This document digs into the Layered Constructor Pattern, a design pattern that recurs throughout Apex Stem's Usecase layer, as a standalone topic. Read this after Handler-Usecase Architecture — if you came away wondering "what exactly are these two constructors?", this is your answer.
The Layered Constructor Pattern is the design pattern that lets a single Usecase class hold both a clean production API and flexible test-side dependency injection. It functions as the standard play for keeping testability without needing a DI container — even in Apex, where there isn't one — while still refusing to create half-baked objects.
Salesforce's own blog shows a sample with exactly this constructor shape (the passage). This is not a bespoke idea.
What the Layered Constructor Pattern Is
You place two constructors side by side inside one Usecase class.
- A
publicconstructor: for production. Receives only the business inputs (Ids, SObjects, request DTOs, etc.). - A
@TestVisible privateconstructor: for tests. Receives business inputs plus every dependency (data access, helper components, etc.) as arguments.
The public constructor delegates to the private one with this(...), passing null for every dependency. On the private side, null-coalescing (e.g. ?? new Eloquent()) swaps in the production default. With this shape, dependencies are completely hidden from the production caller, and tests can inject mocks.
In Apex Stem's Usecase layer, this is the standard form.
Why This Form Is Necessary
Open Exactly One Seam
The point of this pattern isn't "write two constructors". It is to gather the place where things can be swapped — the seam — into the single point where the object is completed.
The private constructor is the one and only initialisation path in this class. Fields are filled here and nowhere else; dependencies are decided here and nowhere else. So the place a test intervenes is also just this one point.
With a single seam, several things hold at once.
- Production callers never see the seam.
new Xxx(ids)returns something complete, every time - Tests intervene without adding anything. No setters to grow, no visibility to loosen, no test-only flags
- Every path builds the same invariants. Whether you came through public or private, one body of code runs
Scatter the seam instead — three setters, two initialisation paths — and "which calls make it complete" leaks out of the class. This shape is what prevents that.
The Seam Exists to Keep Tests From Going Laxer Than Production
Making things swappable isn't only about speed. It matters just as much that tests don't get laxer than production.
Swap IEloquent for MockEloquent at this seam and the mock hands back records knowing what the production query (Scribe) selected. Touch a field it never selected and the unit test throws. Had you passed a hand-assembled SObject straight in, you would have got null, passed quietly, and found out in production.
Because the seam sits here, the mock inherits production's contract. This isn't "bending the design for tests" — it is fixing the design and having tests move closer to production.
Apex Has No DI Container
A DI container that auto-wires dependencies — like Java's Spring or PHP's Laravel — isn't in Apex's standard library. There's no inject annotation, and no automatic constructor resolution either.
That means how to inject dependencies has to be designed by hand. The Layered Constructor Pattern is one answer to that hand-rolled DI question.
"Take Everything Through the Constructor" Is Painful for Callers
A naive approach would be a single public constructor that takes both the business inputs and every dependency.
// Anti-pattern: production has to assemble every dependency too
new CreateOpportunityFromAccountUsecase(
accountId,
new AccountReader(new Eloquent()),
new OpportunityEligibilityValidator(),
new OpportunityMapper(),
new Eloquent()
).invoke();
Writing two new Eloquent()s every time a Trigger handler invokes a Usecase isn't realistic. Production code gets noisier, and the area you need to touch to update a default expands.
"Parameter-less Constructor + Setter Injection" Creates Half-Baked Objects
Another approach is to create the instance with a parameter-less constructor and inject dependencies via setters.
// Anti-pattern: setter injection creates "half-baked objects"
CopyAccountIndustryToOpportunityUsecase usecase
= new CopyAccountIndustryToOpportunityUsecase();
usecase.setOpportunityIds(opportunityIds);
usecase.setFetchEloquent(new Eloquent());
usecase.invoke();
This violates the Handler-Usecase Architecture's core principle of "don't create half-baked objects". A forgotten setter call's NullPointerException is hard to surface, and you lose the invariant "the moment the constructor is called, this object is complete".
The Layered Constructor Pattern Resolves Both
Under the Layered Constructor Pattern,
- The production side only needs to pass the business inputs through the public constructor
- The test side assembles the object with all dependencies, in complete state, through the private constructor
- Both constructors preserve the property "complete the moment they're called"
In place of a DI container, the combination of the compiler and @TestVisible plays the role.
Structure
Roles of the public and private Constructors
| Constructor | Visibility | Arguments | Role |
|---|---|---|---|
| public constructor | public | Business inputs only | The production entry point. Delegates to the private constructor |
| private constructor | @TestVisible private | Business inputs + all dependencies | The single object-initialization site. If a dependency is null, swap in the production default |
"The public constructor is minimal; the private constructor is complete" is the rule. Dependencies are hidden from the production caller, and tests can swap every dependency — both at once.
Delegation and null-coalescing
The public constructor delegates to the private one with this(...), passing null for every dependency. On the private side, the null-coalescing operator (??) swaps in the production default.
public CopyAccountIndustryToOpportunityUsecase(Set<Id> opportunityIds) {
this(opportunityIds, null); // dependency null; delegate to private constructor
}
@TestVisible
private CopyAccountIndustryToOpportunityUsecase(
Set<Id> opportunityIds,
IEloquent eloquent
) {
this.opportunityIds = opportunityIds;
this.eloquent = eloquent ?? new Eloquent(); // null → production default
}
From the caller's perspective, production is one line — new CopyAccountIndustryToOpportunityUsecase(ids) — and tests swap the dependency with new CopyAccountIndustryToOpportunityUsecase(ids, mock).
Example 1: Leaf Usecase (DI'ing IEloquent)
Let's revisit CopyAccountIndustryToOpportunityUsecase from Step 3 of the Apex Stem Introduction Guide through the Layered Constructor Pattern lens.
public with sharing class CopyAccountIndustryToOpportunityUsecase {
@TestVisible static final String LBL_FETCH = 'oppFetch'; // opportunity + parent account fetch
@TestVisible static final String LBL_UPDATE = 'oppUpdate'; // opportunity update
private final Set<Id> opportunityIds;
private final IEloquent eloquent;
private Trace t = Trace.of('Copy parent Account industry to Opportunity');
public CopyAccountIndustryToOpportunityUsecase(Set<Id> opportunityIds) {
this(opportunityIds, null);
}
@TestVisible
private CopyAccountIndustryToOpportunityUsecase(
Set<Id> opportunityIds,
IEloquent eloquent
) {
this.opportunityIds = opportunityIds;
this.eloquent = eloquent ?? new Eloquent();
}
public void invoke() {
// (full text in step 3 of the Introduction Guide)
}
}
Two points to notice.
- A single
IEloquentmultiplexed by label, split by purpose. By labeling fetch (LBL_FETCH) and update (LBL_UPDATE) on the sameIEloquent, tests can run independent scenarios — "fetch succeeds but update throws" — on a singleMockEloquent. Beforelabel()arrived in v2.1, you would DI two separateIEloquentfields (still works, but the constructor swells). ?? new Eloquent()for the production default. Dependencies are completely hidden from the production caller —new CopyAccountIndustryToOpportunityUsecase(ids)is all you need.
In this way, receiving data access through an abstraction (IEloquent) and DI-ing it via the Layered Constructor Pattern is the basic form for leaf Usecases. When the Usecase only depends on IEloquent, the v2.1+ recommendation is to consolidate it into one IEloquent with label multiplexing. Splitting different kinds of dependencies (IEloquent + Reader + Validator + Mapper, etc.) is covered in Example 2.
Example 2: Orchestrator Usecase (DI'ing Component Classes)
In an orchestrator Usecase that bundles multiple steps, dependencies expand beyond IEloquent into component classes like Reader / Validator / Mapper. The Layered Constructor Pattern's shape doesn't change, though.
As an example, consider a Usecase that "creates one Opportunity from an Account Id". The steps:
- Fetch the Account (
AccountReader) - Validate that we may create an Opportunity (
OpportunityEligibilityValidator) - Assemble an Opportunity from the Account info (
OpportunityMapper) - Insert the Opportunity (
IEloquent)
public with sharing class CreateOpportunityFromAccountUsecase {
private final Id accountId;
private final AccountReader accountReader;
private final OpportunityEligibilityValidator validator;
private final OpportunityMapper mapper;
private final IEloquent insertEloquent;
private Trace t = Trace.of('Create an opportunity from an account');
public CreateOpportunityFromAccountUsecase(Id accountId) {
this(accountId, null, null, null, null);
}
@TestVisible
private CreateOpportunityFromAccountUsecase(
Id accountId,
AccountReader accountReader,
OpportunityEligibilityValidator validator,
OpportunityMapper mapper,
IEloquent insertEloquent
) {
this.accountId = accountId;
this.accountReader = accountReader ?? new AccountReader(new Eloquent());
this.validator = validator ?? new OpportunityEligibilityValidator();
this.mapper = mapper ?? new OpportunityMapper();
this.insertEloquent = insertEloquent ?? new Eloquent();
}
public void invoke() {
this.t.start();
IEntry accountEntry = this.accountReader.fetch(this.accountId);
this.validator.assertEligible(accountEntry);
Opportunity opp = this.mapper.toOpportunity(accountEntry);
this.insertEloquent.doInsert(opp);
this.t.finish('Created 1 opportunity.');
}
}
The shape is identical to Example 1. The only change is the number and kinds of dependencies. The public constructor still takes only business input (accountId), and the private one swaps null into production defaults.
AccountReader itself follows the same "constructor receives IEloquent" pattern (the "don't create half-baked objects" principle), and new AccountReader(new Eloquent()) is enough to complete it. Components without external dependencies, like OpportunityEligibilityValidator and OpportunityMapper, are assembled with a parameter-less new.
For how to split component classes (
Reader/Validator/Mapper) and the responsibility-division guidelines, see "Component Classes" in Handler-Usecase Architecture. This document's focus is how to inject them into the Usecase.
This Four-Way Split Is Not the Goal
Example 2 exists to show that the shape doesn't change as the kinds of dependencies grow — not to recommend splitting into four.
What justifies a split is whether there is a seam you want to swap. AccountReader touches the database, so it earns one. Carve out pure logic you never need to swap, purely because "I want to DI it", and the trade stops paying.
- Arguments start getting relayed around (parameters that exist only to pass data between components)
- Code working on the same data lands in different classes and queries the same records twice
- The class you carved out ends up half-formed, used by nobody else
The rule of thumb is: keep together what shares the same knowledge (context), and split where the knowledge changes. Lumping things together indiscriminately is the opposite failure — a class made the dumping ground for shared code belongs to no context at all.
Ask this once after you've implemented, and the granularity settles.
Is there anything to consolidate or streamline? Avoid over-consolidating: bring together only knowledge that shares a context.
The reservation about granularity itself is written up in "A reservation about granularity" in Handler-Usecase Architecture. What's shown here is the granularity we consider reasonable as of 2026.
What Changes in Tests
A Usecase that adopts the Layered Constructor Pattern gains the following freedoms in tests.
- Per-purpose independent dependency swap. For a single
IEloquent, label-multiplex it (e.g.LBL_FETCH/LBL_UPDATE) vialabel(); for an orchestrator, split component classes across separate fields — pick the granularity that fits. Either way, independent scenarios fall out naturally: "fetch succeeds but update throws", "fetch returns empty and update isn't called". Verifications that are hard to express with one label-less sharedIEloquentcome together painlessly. - Mock / fake / real component classes at any granularity. In orchestrator Usecases, you can mock
AccountReaderwhile using the realOpportunityMapper, per test. "Keep the logical core running on the real implementation while closing off only the external I/O" — that kind of test design becomes natural. - Zero changes to production code. You don't need to add a constructor or a setter to production code to enable tests. The
@TestVisibleprivate constructor is already there as the dedicated test entry point.
For concrete test code, see Step 4 of the Apex Stem Introduction Guide. Injecting a single MockEloquent, feeding the read through attach(LBL_FETCH, ...) and checking the write with upsertedRecordsAt(LBL_UPDATE) is the canonical use of the Layered Constructor Pattern.
Read Next
- Handler-Usecase Architecture: the core Apex Stem architecture that includes the Usecase layer where the Layered Constructor Pattern appears
- Apex Stem Introduction Guide: walking through the 4 steps, including the Layered Constructor Pattern, with working code
- Test Strategy: how to write tests for the Handler and Usecase respectively