Layered Constructor Pattern

Apex Stem Docs
Apex StemHandler-Usecase ArchitectureDesign PatternTestingSalesforceApex
A design pattern that combines a clean production API and flexible test DI in one Usecase class, without a DI container.

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 public constructor: for production. Receives only the business inputs (Ids, SObjects, request DTOs, etc.).
  • A @TestVisible private constructor: 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.

APEX
// 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.

APEX
// 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

ConstructorVisibilityArgumentsRole
public constructorpublicBusiness inputs onlyThe production entry point. Delegates to the private constructor
private constructor@TestVisible privateBusiness inputs + all dependenciesThe 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.

APEX
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.

APEX
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 IEloquent multiplexed by label, split by purpose. By labeling fetch (LBL_FETCH) and update (LBL_UPDATE) on the same IEloquent, tests can run independent scenarios — "fetch succeeds but update throws" — on a single MockEloquent. Before label() arrived in v2.1, you would DI two separate IEloquent fields (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:

  1. Fetch the Account (AccountReader)
  2. Validate that we may create an Opportunity (OpportunityEligibilityValidator)
  3. Assemble an Opportunity from the Account info (OpportunityMapper)
  4. Insert the Opportunity (IEloquent)
APEX
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) via label(); 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 shared IEloquent come together painlessly.
  • Mock / fake / real component classes at any granularity. In orchestrator Usecases, you can mock AccountReader while using the real OpportunityMapper, 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 @TestVisible private 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.