How we build: duplication vs premature abstractions

January 27, 2026
3
min read
Engineering

Most engineering teams optimize for DRY (Don't Repeat Yourself) and flexibility. We optimize for duplication and constraints. We add correct abstractions when we can - else we duplicate. Here's why we ship faster.

Conventional Wisdom We Reject

The industry treats duplication as a cardinal sin. Teams abstract at the first sign of similarity. Junior and mid-level engineers  push DRY, then suffer when new behavior breaks shared abstractions. The result? "Clean Architecture" with controllers, use cases, interfaces, DTOs, presenters, adapters, and mappers. "Clean architecture" where a simple feature touches 8-12 files.

This approach assumes you need to build generic handlers that can accommodate any future requirement, adding configuration flags for optionality. It's wearing a tuxedo to the beach.

Our Approach to Automating AR at Monk:

Embrace duplication until patterns emerge, then extract the right abstraction.

We believe that duplication is 100% okay. And it is cheaper than the wrong abstraction.

We are inspired by some of the engineering principles at Ramp, including #6 here (worth a read - click here).

Three Layers at Monk

Router to Service to DAO.

What we DON'T have:

// ❌ Typical "flexible" handler
const handleRequest = ({
 type,
 useCache,
 skipValidation,
 allowPartial,
 legacyMode,
 experimentalFeature,
 ...opts
}: GenericHandlerOpts) => {
 if (type === 'A' && !legacyMode) { ... }
 else if (type === 'B' && useCache) { ... }
 // 200 lines of conditionals
}


What we have instead:

// ✅ Specific, boring handlers
const handleTypeA = ({ id }: { id: string }, tx: TransactionType) => { ... }
const handleTypeB = ({ id }: { id: string }, tx: TransactionType) => { ... }


Two functions. Same signature.

How we think through duplication

We deliberately duplicate code until patterns become clear. This gives us 3 advantages:

  1. No premature abstraction = We don't guess at future requirements
  2. Real patterns emerge = By the third instance, the right abstraction is obvious. The perfect abstraction is what we are after
  3. Easy to refactor = Duplicated code is straightforward to consolidate when ready

Other params we think through when handling duplication and onboarding new engineers:

Static methods where possible. Require tx param for DB operations. No optional flags that create complexity.

We think that several things are required to get good abstractions right: (1) validate against real world examples before writing code, (2) testing by thinking through implementation, meaning if the code feels clunky - the abstraction might be wrong, (3) accepting upfront cost, (4) thinking through this in layers from Router to DAO.

When to Abstract vs. Duplicate:

Duplicate when:

  • You've only seen the pattern once or twice
  • The nuances between similar things are critical
  • You don't yet understand the underlying problem
  • The cost of the wrong abstraction is high

Abstract when:

  • You've seen the same problem three or more times
  • The pattern is clear across real examples
  • The abstraction solves seemingly unrelated problems
  • Implementation sketches feel elegant and simple

Obvious trade-offs we face:

Upfront investment in proper abstractions takes longer initially but saves months collectively in the medium term. You trade write complexity for execution simplicity—making the most important logic simple and performant.

Wrong abstractions compound. You'll spend more time fighting them than building features. As the 2024-25 developer community has realized: waiving DRY reduces technical debt, not the other way around.

Some of the tools we use at Monk to enable shipping fast:

Graphite = Small PRs kill the urge to "clean up" with abstractions. More on how we use Graphite and stacked diffs here

Braintrust = real-world signals tell us what to abstract. Evals are the modern tests is becoming a common phrase for a reason. More on how we use Braintrust here

Linear = tight scope means we ship

Results:

We are biased but we'd like to think that we are able to deliver quality code to our customers fast. No "lasagna code".

In systems that need to be reliable (such as AR) boring/duplicated code works great. And when patterns emerge - yes, we abstract.

Then those abstractions compound. And they make us faster than a variant of the Monk eng team that would be stuck with a premature DRY decision.

Interested in building with us? We are searching for stellar engineers. More here and here