Born from a legacy org

Apex Stem"We can't write tests"is where this started

The tools didn't come first. They were built so that tests could be written.
One trunk — Handler-Usecase — and the OSS that grew out of it.

Five years in the making

The Story

How a five-year-old Salesforce org turned into Apex Stem — one Usecase at a time.

The Challenge

A five-year-old Salesforce org, with trigger-on-trigger debt and assertions that didn't really assert.

fflib felt too heavy for a small team. Raw Apex felt too unstructured for the long run.

I wanted something AI coding assistants could actually follow — a convention small enough to fit in CLAUDE.md.

What emerged

Rebuilt incrementally — one Usecase, one mockable query at a time.

ApexEloquent came first. ApexBlueprint, ApexTrace, and ApexTools followed as the needs surfaced.

The stack you see today is what I'd want if I had to start the refactor over.

The Stack

Four OSS libraries, one architecture. Each can be adopted independently — or together as a unified stack.

Data Access

ApexEloquent

SOQL/DML ORM with a mock-first testing API. Used for unit-testing the Usecase layer.

SELECT-omission detection via MockEntry

GitHub →
Test Data Factory

ApexBlueprint

Declarative test data factory. Used for integration-testing the Handler layer with real DML.

Layered topological sort for bulkified DML

GitHub →
Lifecycle Logging

ApexTrace

Usecase lifecycle logging.Trace.of() with start / skip / finish / log.

TraceFlow-based behavior assertions in tests

GitHub →
Foundation

ApexTools

TriggerHandlerbase class and an HTTP request wrapper with DI support.

Field-change detection helpers for after-update handlers

GitHub →

Plus a thin Result DTO convention for the LWC ↔ Apex boundary. Start with one library — start anywhere.

Handler-Usecase Architecture

Two layers. One rule per layer. The rest is up to you.

Handler Layer

Integration tests · ApexBlueprint

Entry points — Trigger, Batch, REST, Flow, Schedulable. Absorbs the entry-point ceremony, then hands off to a Usecase.No business logic here.

Usecase Layer

Unit tests · ApexEloquent (mock DB)

Business logic. Only invoke() is public. Constructor takes every dependency —no half-built objects.

Laravel-MVC philosophy

Define the trunk only. Reader / Validator / Mapper emerge from the business — they're never enforced.

Tests map 1-to-1

Handler → integration with real DML. Usecase → unit with mock DB. Same convention everywhere.

AI-friendly by design

The whole convention fits in 30 lines of CLAUDE.md. AI codegen doesn't drift — there's nowhere to drift to.

Proof

Saying "it feels like it's working" gets us nowhere, so we doubted it ourselves and measured.

Doesn't mocking make tests worse?

Mock

83.4%

vs

Real DML

79.3%

Measured with mutation testing (planting bugs on purpose and checking whether the tests notice). We planted 334 of them and compared how many the mock version caught against the real-DML version.It didn't drop.

How much faster, exactly?

Mock

15ms

vs

Real DML

272–1,062ms

Median per method. Measured across two orgs in production use.

Won't it slow down as the org grows?

Constant time

The unit layer runs no DML, so only the real-DML side pulls away.

All of these are real measurements, but not large ones (one target class, one run each). Read them as "this is what we measured", not as "this is proven".

Get Started

You don't have to rewrite anything. Start with one query — grow from there.

Take the first step

~2 minute read · then continue to the full guide