Filed at 10,000 m, lands at 300 m.Architecture

A consistent coding framework is what makes 100 developers behave like one team

A framework is a core set of classes, the conventions around them, and someone enforcing both. Its product is that every team’s code has the same shape, so anyone can review anyone’s work and the next platform change is three edits, not every class. That only holds if someone owns it.

In this descent, 3 stops

Every enterprise org has a coding framework. Some were designed. Most accreted. A few are three delivery partners’ house styles stacked in the same repository, and you can date a class by which one it uses.

The question is never whether you have a framework. It is whether it is consistent, and whether the next developer can tell which parts are load-bearing.

I work in a large enterprise with 100+ developers spread across multiple teams and vendors, all sharing a monorepo of more than 20,000 components. The framework holding that together is about ten base classes, a wiki page with a diagram, and a review checklist that cites both. The classes are the part you can see. The rest of this is about the parts you cannot.

You are funding decisions, not classes

Fund the framework as a product with an owner and a budget line, or accept that your codebase will be the sum of every team’s private habits. Plenty of orgs have the second by default and describe it as the first.

A coding framework is a list of decisions made once. Where queries live. Where writes happen. How a test fakes the database. How asynchronous work retries and how deep it is allowed to chain. Without the framework, each of those is re-decided per ticket, by whoever is closest to it, and each answer is locally reasonable. What you get is a few hundred reasonable answers nobody can read end to end. You pay for that in review time, defects, onboarding and every partner transition, indefinitely.

Think of a shared kitchen. A framework is the drawer labels. The kitchen still works without them, right up until you hire a second chef.

The failure mode at this altitude is the unfunded framework. It exists, because the codebase needed it, but nobody is paid to maintain it, so it stops keeping up with the platform. Teams route around the stale bits, the “framework way” and the “fast way” start coexisting, and eventually the rot gets blamed on the platform. The fix is boring: a named owner, a small standing allocation, and a place where changes get decided.

Where it does not apply: one or two teams, or an org where most logic is configured rather than coded. They get the ceremony without the scale to pay for it. Below that line, a page of conventions and a code review will do. Above it, the framework is cheaper than the review time it replaces.

The layers only pay off at the joins

Consistency means the same shape for the same problem, not identical code. The framework’s real product is one vocabulary. Whether a request arrives from a Lightning Web Component controller, a Flow action, a batch job or a trigger, it moves through the same parts: entry point, service, selector, unit of work. A developer who has read one feature can read all of them.

Three parts, one of which fits on a diagram

A framework is base classes, conventions, and enforcement. The base classes are the part you can draw, and which ones you have matters less than that everyone uses the same ones. In the framework I work with there are about ten. Trigger handler, module base, selector base, unit of work, database wrapper, recursion control, logger, async worker, mocking utility, test data factory. Yours will differ. fflib, the open source Apex Enterprise Patterns library, draws the same territory with different boxes, and so does every home-grown framework worth the name.

The conventions say when each one is used and what a class is called. The enforcement is the lint rules and the review checklist that notice when someone did not.

Base classes without conventions become optional. Conventions without enforcement become folklore. Enforcement without base classes is a checklist of things nobody has a good way to do.

Where the compound benefit lives

The value is in the intersections, which means all the layers have to be present. An example, using the framework I work with.

Follow one record through. A Case is inserted and the trigger fires once, into a handler that runs the registered modules in a fixed order with one shared unit of work. A module needs a related record that may not exist locally yet, so it asks a selector. The record is missing, so rather than making a callout inside a trigger, the module registers an async worker on the unit of work and finishes.

After commit, the worker runs and hands off to a service. The service queries through selectors, calls an integration adapter, and registers the resulting records on its own unit of work. That commit updates the Case, which fires the trigger again, and recursion control filters out the records that have not changed since the first pass.

Count the seams. The module never issued DML (Data Manipulation Language) itself and read only through a selector, so with the selector stubbed it is unit-testable in memory. The unit of work delegates every insert and update to one database wrapper, so a single mock captures all the writes for verification. The async worker got retry on lock failure and a depth limit for free, because those live in the base class. The second trigger pass did nothing expensive, because recursion control saw the same field values.

None of that is a feature of any one layer. Remove the database wrapper and the mock has nothing to hold. Remove recursion control and the async worker’s commit reruns every module. Half a framework costs the whole learning curve and returns a fraction of the value.

The payback is back-loaded

A framework costs the most in its first year and pays the most in its fifth. The first feature built on it is slower than the same feature built freehand, because someone has to learn the layers. The fiftieth is faster, because the layers are already there. By the five hundredth, the org without one has a codebase nobody dares touch. The saving is no longer hours. It is whether the change gets made at all.

Every year a codebase without a framework accumulates habits: one team’s trigger pattern, a partner’s query style, a contractor’s idea of error handling. Each is cheap when written and expensive forever after, because every reader has to work out which one they are looking at. A framework does not stop the codebase growing. It stops the number of ways to read it growing with it. The newest reader is the coding agent, and it cares more about that than anyone. Consistency was always cheaper to review. It is now cheaper to generate.

None of this shows up in the sprint where the framework is funded, which is why it so often gets funded late, after the habits have set.

What it costs

The learning curve is real. A new developer meets ten base classes and a mocking library before changing a field value. That is only worth it if the conventions are written down somewhere they will be read.

The maintenance cost arrives in lumps. When the platform changes something fundamental, the framework absorbs it, which is the point, but absorbing it means touching every base class at once.

The current example is Apex version 67.0, which runs database operations in user mode by default. In an org without a framework that change lands on every class individually. In an org with one, it lands on the selector base, the database wrapper and the unit of work, plus whatever was granted an exception. That is a good trade.

The third cost is the registration hotspot. Every factory that maps objects to selectors, modules or services is a file every team edits, and I covered the merge conflicts that causes in the Apex enterprise patterns post.

Build or adopt

Either, provided the org owns the opinions. Adopting fflib, or Trigger Actions Framework for the automation layer, and wrapping it with your own conventions is a perfectly good framework.

Adopting either and treating its defaults as your standard means running someone else’s framework. Its opinions on DML order, on user mode and on what a domain class is become yours by omission.

Building your own buys you conventions that fit the org and a base class you are allowed to change. It costs you a team that has to keep it current, and a mocking story, which is why the home-grown frameworks I have seen still import fflib-apex-mocks. The mocking library is the one piece worth borrowing whole.

The ticket already says where the code goes

On a Tuesday, the benefit is that nobody argues about structure. The ticket describes a behaviour and the framework decides where each part lives. The pull request is a service method, a selector method, possibly a module, and a test, in files a reviewer can name before opening them. Review then checks conformance and correctness, not taste, which is the only kind of review that survives a hundred developers.

The other benefit is the test. Every write goes through the unit of work and every read through a selector, so a service can be tested with both stubbed and nothing touching the database. The assertions check what was registered, not what landed in the database. That test runs in seconds and fails for one reason. It is the payoff the rest of the framework exists for, and it is the one a team that adopted “just selectors” never gets to see.

The benefit that arrived most recently is that the agent stops guessing. An agent working in a codebase with one shape needs one worked example and the conventions in its context. Point it at a ticket and it finds the selector, extends the service, registers on the unit of work and writes the test the reviewer expects, because there is only one shape to find. Point it at a codebase with a pattern per team and it picks one, often whichever file it read last, and the review becomes an argument about which pattern it should have picked. The framework does for the agent what it did for the new starter, and the agent starts fresh on every ticket.

Enforcement without a police force

You cannot keep 100 developers consistent by reminding them. Automate every rule a machine can check, put the rest on a short review checklist, and give one person or the governance team the job of deciding the exceptions.

The machine-checkable rules are the ones that last. No SOQL (Salesforce Object Query Language) outside a selector. No DML outside the unit of work or the database wrapper. No callouts from a trigger context. Every trigger delegates to a handler on the first line. Each of those is a regular expression or a lint rule, and each is flagged before a human looks at the pull request.

The human-checked rules are shorter and shakier. Business logic in a service, not a controller. Modules that read but do not write. A name that says what the class is for. These go on the checklist, and the checklist gets read only if it is short.

They are also the rules an AI reviewer is good at. Give a model the conventions as context and it will flag logic in a controller, or a module doing writes, on every pull request. It does not get tired on its sixth review and it does not skip the checklist. Let it do the first pass. The human reviewer then argues with its findings rather than hunting for them.

Exceptions need a door. A developer who genuinely needs a raw query for performance, or a callout the async path cannot express, writes the reason in the pull request. The framework owner decides, and the rule is that they decide within a day. Without that door, the exceptions happen anyway and nobody writes them down.

Where this does not hold

A framework adopted mid-flight, into an org with a decade of history, will coexist with legacy code for years. The rule is that new code follows the framework and touched code migrates toward it, and nobody launches a rewrite. Lint the new files strictly, warn on the old ones, and let the ratio move.

Something to do this week

Take the last ten merged pull requests in your repository. For each new or changed Apex class, write down which of your framework’s layers it belongs to, whatever your framework calls them. Anything you cannot place in under a minute is one of two things. Either the framework has a gap and the layer should exist, or the developer routed around it and the enforcement has a gap. If you do not know who the framework owner is, you have found the first item.

Nathan Avatar

More about Nathan

Where next

Your AI code reviewer still needs a linter

A linter is a smoke alarm and an LLM is a building inspector. Keep linting as the deterministic gate (security rules on every touched file, everything else on changed lines) and let the model review intent and suggest repairs that go back through the same gate.

3,000 m to 300 m. AI and agents, 10 minute read.