Handler-Usecase Architecture
This document covers Apex Stem's core architecture — the Handler-Usecase Architecture — digging into both the design philosophy and the structure. Read this after you've worked through the Apex Stem Introduction Guide, when you want to understand "what exactly is a Handler, and what exactly is a Usecase?".
The Handler-Usecase Architecture is a lightweight application architecture for Salesforce Apex development. Every Apex process is designed in two layers: a Handler (the entry point) and a Usecase (the business logic).
What the Handler-Usecase Architecture Is
It is a third way — neither a heavyweight multi-layer architecture like fflib, nor the chaos of stuffing everything into a Trigger or a single class.
It was conceived and proven while rebuilding the chaos of a Salesforce org that had been operated for 5 years. The conventions you have to remember are just "two trunk layers (Handler and Usecase)" and "the basic principles of object orientation". Everything else is left to the field.
Why Two Layers
In the Laravel-MVC Lineage
The Laravel web framework defines only Controller, Model, and View, leaving components like Service, Action, and Repository to the community. Rails does the same.
The Handler-Usecase Architecture stands in that lineage. Handler and Usecase are the trunk, and we don't prescribe categories for the components that appear beneath them (Reader / Validator / Mapper, and so on). It is exactly the opposite philosophy from fflib, which prescribes Selector / Domain / Service / UnitOfWork all at once.
Why "Don't Over-Prescribe"
- Different businesses need different components. Some need a Reader; some need a Validator. Picking all categories in advance creates ones that never get used.
- Prescription leads to formalism. After 5 years of operation, you will always end up with "a class named Selector, but the inside is straight-line code" — name-only structures.
- A growth opportunity for juniors. Thinking through "what to call from the Handler" and "where to split out a component" is itself training in object-oriented design.
- In the age of AI, too many conventions become noise. Telling an AI coding assistant "follow these 12 patterns" is more error-prone than "two trunks + OOP principles".
Connection to the "Mino-Driven Book"
The principle "don't create half-baked objects" from Good Code / Bad Code: Design Fundamentals (a Japanese design primer commonly nicknamed the Mino-Driven Book) is the core of the Usecase. Receive everything you need through the constructor and don't bolt on state via setters later. This is the Apex version of the Value Object thinking in Effective Java and Domain-Driven Design.
The Handler Layer
Handler responsibilities
The Handler's responsibility is only to absorb the entry-point-specific conventions and hand them off to the Usecase. Business logic doesn't live here. Filtering target records is allowed, but nothing more. Its role is close to a Controller in Laravel.
Five Kinds of Entry Points
Apex has multiple kinds of entry points, and we prepare a Handler for each.
| Type | Entry Source |
|---|---|
| TriggerHandlers | DML triggers (before/after × insert/update/delete) |
| BatchHandlers | Batchable / Schedulable |
| RestHandlers | @RestResource |
| FlowHandlers | @InvocableMethod (called from Flow) |
| SchedulableHandlers | Pure Schedulable |
The Fixed Trigger.cls Pattern
The trigger file follows Salesforce convention and uses a fixed form. Declare all seven events and call the Handler in one line. No logic in the trigger file itself.
trigger Opportunity on Opportunity(
before insert,
before update,
before delete,
after insert,
after update,
after delete,
after undelete
) {
(new TriggerOppHandler()).execute();
}
The TriggerHandler Base Class
Each Handler extends the TriggerHandler base class (provided by ApexTools) and overrides only the hooks it needs. All hooks are protected virtual; hooks you don't override do nothing.
The main hooks are beforeInsert / beforeUpdate / beforeDelete / afterInsert / afterUpdate / afterDelete / afterUndelete, plus andFinally, which is always called last. A helper for "narrow down to records where specific fields changed" (getUpdateRecordIdsWithChangedFields and friends) is also provided.
Code Example
Here we call the CopyAccountIndustryToOpportunityUsecase (the Usecase that copies the parent Account's industry to the Opportunity), already covered in the Apex Stem Introduction Guide, from a Trigger.
public with sharing class TriggerOppHandler extends TriggerHandler {
protected override void afterInsert(Map<Id, SObject> newRecordsMap) {
Set<Id> opportunityIds = newRecordsMap.keySet();
(new CopyAccountIndustryToOpportunityUsecase(opportunityIds)).invoke();
}
}
All the Handler is doing is "collect Opportunity Ids from Trigger.new and hand them off to the Usecase". The logic of copying the industry doesn't live here at all — that's the Usecase's job.
When you only want to call a Usecase if specific fields changed, narrow down with the base class's helper.
protected override void afterUpdate(Map<Id, SObject> newMap, Map<Id, SObject> oldMap) {
Set<Id> needIds = this.getUpdateRecordIdsWithChangedField(Opportunity.AccountId);
(new CopyAccountIndustryToOpportunityUsecase(needIds)).invoke();
}
The Usecase Layer
Usecase responsibilities
The Usecase implements a single piece of business logic. The only public method is invoke() — everything else is private. The return type of invoke() is chosen by the nature of the process (a Result DTO for Usecases called from LWC, often void for those driven by Trigger or Batch, and so on). Its role is close to a Service or Action in Laravel.
Don't Create Half-Baked Objects
The Usecase receives every dependency it needs through the constructor. You don't bolt on state later via setters. The state "the moment the constructor is called, this Usecase is complete" is preserved.
Two Constructors
The Usecase has two constructors.
- The
publicconstructor. For production. Receives only the business inputs and creates dependencies like data access by default. - The
@TestVisible privateconstructor. For tests. Receives dependencies as arguments so tests can inject mocks.
public with sharing class CopyAccountIndustryToOpportunityUsecase {
@TestVisible static final String LBL_FETCH = 'oppFetch';
@TestVisible static final String LBL_UPDATE = 'oppUpdate';
private final Set<Id> opportunityIds;
private final IEloquent eloquent;
private Trace t = Trace.of('Copy parent Account industry to Opportunity');
// public: for production, receives only business inputs
public CopyAccountIndustryToOpportunityUsecase(Set<Id> opportunityIds) {
this(opportunityIds, null);
}
// private (@TestVisible): inject IEloquent in tests
@TestVisible
private CopyAccountIndustryToOpportunityUsecase(
Set<Id> opportunityIds,
IEloquent eloquent
) {
this.opportunityIds = opportunityIds;
this.eloquent = eloquent ?? new Eloquent();
}
public void invoke() {
// ... business logic (full text in step 3 of the Introduction Guide) ...
}
}
This "split the constructor in two — one for production, one for tests" style is the Layered Constructor Pattern. For details see Layered Constructor Pattern. A hands-on example is in Step 3 of the Introduction Guide.
Single Usecase vs Orchestrator Usecase
There are two shapes of Usecase.
- Single Usecase. A small piece of logic that completes within private methods. The
CopyAccountIndustryToOpportunityUsecaseabove is this kind. - Orchestrator Usecase. A larger piece of logic that integrates multiple component classes (Readers, Validators, or even other Usecases).
Both follow the same principles: "only invoke() is public" and "don't create half-baked objects".
Component Classes
The Handler-Usecase Architecture does not prescribe categories for the component classes that appear beneath a Usecase.
The kinds of components that show up depend heavily on the business. "Always create a Reader / Validator / Mapper" is not what we say. Some processes need a Reader; others need a Validator. That call is made on the ground.
There is only one common rule: component classes also follow the "don't create half-baked objects" principle. Receive everything you need in the constructor, and make them objects that can only exist in a complete state.
The split is allowed to be incremental. Start by writing it as a private method inside the Usecase, and split it out into an independent class when it gets complex. It matters not to over-componentize from the start.
Test Strategy (Overview)
The two layers of the Handler-Usecase Architecture map 1:1 onto two test strategies.
| Layer | Test Type | DB Access | Testing OSS |
|---|---|---|---|
| Usecase layer | Unit test | None (mocks) | ApexEloquent (MockEloquent / MockEntry) |
| Handler layer | Integration test | Yes (real DML) | ApexBlueprint (SBlueprint / SOrchestrator) |
Logic coverage happens in Usecase unit tests. The Handler integration test narrows down to 1–3 representative cases of "the Usecase is correctly invoked through Trigger or Batch, and the expected behavior is observed".
For the full test strategy, see Test Strategy. A hands-on example is in Step 4 of the Introduction Guide.
Where Salesforce's Own Recommendation Overlaps
Everything above was arrived at empirically, out of pain. We found out afterwards that Salesforce recommends the same design.
Separation of concerns
From the official blog post Reduce Deployment Test Time with Smarter Apex Test Runs:
business logic and database interfacing should be separate concerns
And the refactoring sequence the post lays out reads as the Handler-Usecase sequence verbatim: carve database access into its own class, abstract it behind an interface, make it swappable by DI, and inject a mock implementation in tests.
The constructor shape is the same too
Here is the sample code from the official post.
public class OpportunityService {
private OpportunityServiceDbHandler dbHandler;
public OpportunityService() {
this(new OpportunityServiceDbHandlerImpl()); // for production
}
@TestVisible private OpportunityService(
OpportunityServiceDbHandler dbHandler) { // for tests (DI)
this.dbHandler = dbHandler;
}
}
Structurally identical to the Layered Constructor Pattern described above. A public constructor for production that delegates inward, and a @TestVisible private one that takes the dependency for tests. The same answer to the same problem.
Two layers of tests
On dividing up tests, the official position is:
The vast majority of your tests should be true unit tests.
Testing triggers, for example, requires real DML execution, as there is no substitute for validating the execution order.
Such tests aren't unit tests; they're integration or functional tests. Use them sparingly: only when you need to test a trigger or a particularly important or complex integration flow.
"Units are the bulk", "triggers need real DML", "keep integration sparing". The same thing the test-strategy table above says.
Where we differ
The official approach is correct, but it means hand-writing a DbHandler class, an interface and a mock implementation per service. Fifty Usecases bring 150 classes along with them. You also write the SOQL assembly and the test-data dependency resolution yourself, every time.
What ApexEloquent did was generalise that DbHandler into a single one.
| The official pattern | ApexEloquent | |
|---|---|---|
| DB access abstraction | hand-written interface per service | one IEloquent |
| Production implementation | hand-written Impl per service | one Eloquent |
| Mock | hand-written Mock per service | one MockEloquent |
| Query assembly | raw SOQL by hand | Scribe (builder) |
| Test data | assembled by hand | MockEntry / ApexBlueprint |
Not an invention. Just the design the official position points at, generalised so you don't hand-write it every time.
Which also means no particular OSS is required to adopt this design. Hand-write it the official way if you like, or use fflib, or Apex Fluently. Apex Stem combines its four OSS projects because they line up directly with the Handler-Usecase test strategy.
A reservation about granularity
We expect the reason for splitting to hold for a long time. How finely to split is a separate question.
We do not currently have grounds to claim that "one Usecase = one piece of business logic" is the optimum level of fineness. Splitting finely has costs too (more classes, a harder time seeing the whole). It is entirely possible that as models improve, a coarser granularity turns out to be enough.
What is written here is the granularity we consider reasonable as of 2026.
Comparison with Other Frameworks
Comparison with fflib
The Handler-Usecase Architecture isn't a rejection of fflib. Different scenes call for different tools.
| Aspect | fflib | Handler-Usecase Architecture |
|---|---|---|
| Philosophy | Prescriptive (Java EE / Spring lineage) | Minimal skeleton (Laravel / Rails lineage) |
| Required concepts | Selector, Domain, Service, UnitOfWork | Handler, Usecase |
| Component prescription | Yes | No (field judgment) |
| Adoption into existing orgs | Rewrite-first | Erodes one method at a time |
| AI integration | Many conventions, easy for AI to get lost | 2 conventions + principles, easy to convey to AI |
| Best fit | When you want to unify a team of 50+ | When 1–5 people want speed and AI integration |
We respect the work Andy Fawcett built into fflib. On that foundation, the Handler-Usecase Architecture is "a different choice for a different scene".
Relationship with Apex Fluently
Recently, another option has emerged: Apex Fluently (by Beyond The Cloud) — an OSS set with 8 libraries (SOQL Lib, DML Lib, Async Lib, Cache Manager and more) framed as "a modern alternative to fflib".
But Apex Fluently deliberately doesn't prescribe an architecture. It doesn't get involved in application layering or responsibility division; it's designed as a "toolbox" where each library can be adopted individually.
So Apex Fluently and the Handler-Usecase Architecture are arguing on different stages. Looking at all three by their position:
- fflib: a heavyweight architecture (Selector / Domain / Service / UnitOfWork)
- Apex Fluently: pure tools (no prescribed architecture)
- Handler-Usecase Architecture: a minimal architecture (Handler + Usecase)
Apex Stem offers both — the architecture (Handler-Usecase Architecture) and the four OSS (the tools). Apex Fluently sits in the same layer as the OSS layer of Apex Stem (ApexEloquent and friends), and doesn't directly compete with the Handler-Usecase Architecture.
The Handler-Usecase Architecture is also tool-independent. In principle, you could use Apex Fluently's libraries under the Handler and Usecase layers. Apex Stem combines ApexEloquent / ApexBlueprint / ApexTrace / ApexTools because they directly align with the Handler-Usecase Architecture's test strategy (Usecase ↔ MockEloquent, Handler ↔ ApexBlueprint), not because the architecture forces that combination.
Position in the Age of AI
The Handler-Usecase Architecture is designed with AI coding assistants in mind.
Light conventions carry a clear practical benefit. AI coding assistants like Claude Code read a file describing project rules (CLAUDE.md). Trying to write fflib's conventions into that file pushes past 200 lines, requires examples, and increases the risk that the AI gets lost.
With the Handler-Usecase Architecture, the core of the architecture comes through to the AI with a short description like the following. In practice, placing this in CLAUDE.md lets Claude Code write Apex in line with the architecture.
## Architecture (Handler + Usecase, two layers)
Every Apex process is designed in two layers: Handler (entry point) +
Usecase (business logic).
### Responsibility separation
- Handler: interprets arguments handed in from the entry point and
invokes the appropriate Usecase. No business logic.
- Usecase: implements a single piece of business logic. Receives
parameters via constructor; only invoke() is public.
### Handler implementation pattern
The Handler does only "condition checking" and "Usecase invocation".
The Trigger file declares all 7 events and calls the Handler in one line.
### Usecase standard pattern
1. Two constructors (public for production / @TestVisible private for tests)
2. Only invoke() is public; everything else is private
3. Dependencies use null-coalescing for production defaults
(eloquent ?? new Eloquent())
### Test strategy
- Usecase unit test: cover logic branches with MockEloquent
- Handler integration test: 1-3 representative cases with real DML
This isn't theoretical. KrileWorks itself uses this description in real projects and collaborates with AI on the code. "With short conventions, the AI follows the architecture" is empirically demonstrated.
Those 23 lines cover the architecture, and nothing else
To avoid a misunderstanding: the conventions actually handed to the AI on a real project do not stop at 23 lines. The apex-architecture skill KrileWorks publishes runs to 393 lines.
That looks like a contradiction until you break it down.
| Part | Lines | What it is |
|---|---|---|
| The architecture itself | 23 | The snippet above: the two layers, the shape of a Usecase, the test split |
| + code examples | 146 | The same thing shown with a real Trigger and Usecase |
| + house rules and platform traps | 393 | Directory layout, naming and the 40-character limit, Constants, the __ identifier trap, when to merge two classes |
The extra 216 lines are not architecture. They answer "where does this class go", "what do I call it", and "where does Salesforce trip you up" — questions you have to answer under any architecture. Adopting fflib would not remove them.
So the honest statement is:
- Communicating the architecture is cheap. That is the Handler-Usecase claim, and 23 lines is the measure of it.
- Operating a project is not cheap. That cost is independent of which architecture you pick.
Only the first is needed to adopt it. Put the 23 lines in CLAUDE.md and the AI starts writing in this shape. The rest can be added later, once your project's own conventions settle.
📦 If you want the full version used on real projects, grab it from Agent Skills.
Hand the URL to your AI and say "read this and keep it" — that is the whole setup.
While fflib was designed in the era before AI, the Handler-Usecase Architecture is probably one of the first architectures in the Apex world that was designed assuming AI codegen.
Read Next
- Apex Stem Introduction Guide: the 4 steps to gradually adopt it in an existing codebase
- Layered Constructor Pattern: the design of the two constructors
- Test Strategy: how to write tests for the Handler and Usecase respectively