The Payments Engineer Playbook

The Payments Engineer Playbook

You're implementing the basics of ledgers wrong

It's not you; it's that DRY and KISS are misguided.

Alvaro Duran's avatar
Alvaro Duran
Jan 14, 2026
∙ Paid

“Don’t Repeat Yourself” is vague advice.

It’s not that duplicating code is a good thing, no. But multiple generations of software engineers have learnt to avoid writing code, to Keep It Simple Stupid, and to prefer ways of building software that are terse and “elegant”.

This isn’t doing you any good.

Let me give you one example of what I mean: enums.

When we have a set of states in which some entity can be, engineers often choose to represent that status in the form of an enum: a finite set of values that it can take.

An account may be an asset, liability, equity, income, or expense, and most often the codebase includes one object with an attribute status that looks like this:

type Kind string

const (
	Asset     Kind = "asset"
	Liability Kind = "liability"
	Equity    Kind = "equity"
	Income    Kind = "income"
	Expense   Kind = "expense"
)

type Account struct {
	Kind Kind
}

This is probably what your favorite AI would suggest. Your senior engineer would, too!

However, this design choice forces you to verify the kind of your account every time you do something with it. How to calculate a balance, or what to do when you withdraw or deposit take different forms depending on whether the account is an Asset, Equity or Liability, Income or Expense.

The blocks of code that must always be present in order to verify that are called guard clauses.

They take the form of a bunch of return statements at the beginning of a Ruby method, or if statements in Python, or the kind of unbearable mess that Java engineers are now numb to.

Again: engineers don’t even see this as an issue. The links on the previous paragraph even laude the “readability” and the “cleanness” of guard clauses.

Multiple generations have been lost in the pursue of elegant, non-DRY code.

The problem with this approach is that engineers are more likely to introduce integrity bugs

This design demands that you use guard clauses all the time. But nothing in the software will tell you which validations you need to do, and what are the consequences of not using them. Engineers who jump into the codebase for the first time risk making real withdrawals with real money because nobody told them that they need to guard against being in production with a guard clause.

Yeah, I’ve seen that happen.

Even the “readability” that these posts claim should be challenged. Using guard clauses, the part where an Asset account is different from a Liability one isn’t apparent in the code; it’s disseminated throughout the codebase, wherever there’s a guard clause. Forming a mental model of what a particular kind of account is about is impossible.

And that feeds into more integrity bugs. Invalid, catastrophic states of your system lurk, undiscovered, silent, and harmful.

A few years ago, Ürgo Ringo from Wise published an article titled Implementing entity states as separate classes, in which he raised the issues with using enums in order to separate entities:

There are three problems with this design:

  1. mutability. Even though business concept Order is mutable does not mean we should give up all the benefits of immutability when implementing it

  2. it mixes a lot of behavior into a single class and hides away important concepts like difference between a pending Order and a confirmed Order. We want significant business concepts to be more prominent in our code.

  3. if we try to do something we should not be doing we will only discover it at runtime when a validation exception is thrown. This is not aligned with the idea of a mistake proof design.

— Ürgo Ringo, Implementing entity states as separate classes

But the solution he proposed is to represent different kinds as different objects. An Account that could be one of 5 kinds becomes 5 objects named AssetAccount, LiabilityAccount, EquityAccount, IncomeAccount and ExpenseAccount.

I don’t like that design either. Yes, I’m done with guard clauses, because which kind of account I’m using is clearly separated from the others. But there’s going to be a lot of duplicated code, because even though they’re different, many of these kinds share functionality.

LiabilityAccounts’ balances are increased with credits, and decreased with debits. But so are EquityAccounts and ExpenseAccounts.

AssetAccounts work the other way around. And so do IncomeAccounts.


I want to teach you a different approach. One that leverages generics.

Generics is just a way to define an extra parameter in the object or function that links inputs and outputs. The change is that you can assume certain inputs to be of a certain type, and enforce that certain inputs are of some type as a result.

For example: you know that adding strings and numbers in Javascript is a mess:

"" + 1          === "1"
"1" + 10        === "110"
"110" + 2       === "1102"
"1102" - 5      === 1097
1097 + "8"      === "10978"

If I create a function that adds two variables, and I’m not careful with using strings or numbers, then all hell can break loose.

I can use Typescript to build a function that adds two items, while ensuring that the two items are of the same type (integer or string) with generics:

function add<T extends number | string>(a: T, b: T): T {
  return (a + b) as T;
}

const s = add("Hello, ", "TypeScript");
console.log(s); // Hello, TypeScript

const n = add(10, 20);
console.log(n); // 30

// Error: Argument of type 'number' is not assignable to parameter of type 'string'.
const invalid = add("Age: ", 25);

It is possible to use guard clauses and get the same result, but notice that generics don’t require writing any extra code in the body of the function. The type system enforces that the two items are of the same type, and thus every other part of the codebase that uses this function is protected.

“But what has this to do with enums?”, you may ask. More than you think.


After nearly a decade building and maintaining large-scale money software, I’ve seen what works (and what doesn’t) about software that moves money around. In The Payments Engineer Playbook, I share one in-depth article every Wednesday with breakdowns of how money actually moves.

If you’re an engineer or founder who needs to understand money software in depth, join close to 2,000 subscribers from companies like Shopify, Modern Treasury, Coinbase or Flywire to learn how real payment engineers plan, scale, and build money software.


Generics are worth it when domain rules are strict, invalid states are expensive and APIs must be hard to misuse. Money software is the textbook example of all three.

In this article, I’m going to show you two examples of how ledgers benefit from using generics.

  • The Easy One: How to enforce that money amounts are always positive on Entries, while letting Balances be negative.

  • The Hard One: How to separate functionality specific to certain Accounts but not others based on kind.

I’ll do this with generics, without duplicating classes, and without using guard clauses.

Sounds interesting? Then read below.

User's avatar

Continue reading this post for free, courtesy of Alvaro Duran.

Or purchase a paid subscription.
© 2026 Alvaro Duran Barata · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture