Hacker's Handbook


Your Finance Stack Is Lying to You

Accidental Complexity in Fintech

Posted: 2025-05-22
Categories: fintech , types , complexity

Your Finance Stack Is Lying to You

Why Untyped Data, Floats, and “Excel Thinking” Are Quietly Costing You Millions

A picture of the plumbing for my heating system with the caption: Simplicity - It's for simpletons.

At a recent fintech conference, I was struck by how much engineering effort, across both startups and enterprises, is spent cleaning up avoidable messes. Not building competitive advantages. Just damage control:

  • Rounding errors in reports
  • Mismatched payment records
  • Inconsistent handling of currency and tax
  • Broken reconciliation across systems

Entire teams, and even companies, exist just to fix these issues. The root cause?

Untyped, context-free data.

The Excel Legacy That Won’t Die

Most financial systems still think like spreadsheets. In Excel, 100 can mean anything: euros, dollars, cents, basis points. There’s no schema. No units. No context.

That thinking leaks into production systems:

  • APIs with vague fields like amount, value, or fee
  • float(8) columns for monetary values
  • Boolean values stored as "yes"/"no"
  • Timestamps with no timezone
  • Magic numbers passed around without meaning

It might be fast to prototype. But it creates long-term ambiguity, and ambiguity compounds into bugs, delays, and failures.

Essential vs Accidental Complexity

Essential complexity is built into the (financial) domain itself, like multiple currencies, tax rules, compliance steps.

Accidental complexity is everything we add on top: floats for money, ambiguous JSON fields, brittle one-off scripts. This article shows how accidental complexity quietly overwhelms the essential.

It’s Worse in Big Companies

This isn’t just a startup problem.

Large enterprises, such as banks, insurance firms, payment providers, layer complex architectures on top of the same assumptions.

To deal with the resulting chaos, they adopt:

  • ESBs (Enterprise Service Buses) to transform one vague schema into another
  • iPaaS (Integration Platform as a Service) tools to sync inconsistent fields across internal platforms
  • Custom middleware and manual workarounds just to keep systems aligned

The irony? Many of these systems exist solely to translate "value": 100 into another format of "value": 100.

It’s not domain complexity. It’s accidental complexity, caused by a lack of enforced structure.

B2B Invoicing Is Still Email and PDFs

If you think we’ve moved past this, look at B2B invoicing.

In 2025, most companies still send and receive invoices via PDFs attached to emails.

Some are exported from ERPs. Others are typed manually in Word. On the receiving end:

  • Amounts are extracted with OCR or regex
  • PO numbers are verified manually
  • Payment terms and VAT rates are interpreted via email follow-ups

Even with e-invoicing formats like Peppol or Factur-X available, adoption is partial and enforcement is weak. Most systems fall back to parsing visual representations of meaning, not actual structured data.

This leads to:

  • Duplicate or late payments
  • Misrouted funds
  • Fraud via spoofed invoices
  • Manual reconciliation loops that delay accounting closes

We’re throwing machine learning and automation at a problem that starts with: Why are we still emailing pictures of invoices?

Floats, the Silent Saboteurs

A particularly nasty example: using floats for money.

0.1 + 0.2 != 0.3

It’s the reason your payment gateway and your ledger might silently disagree.

Using floating-point types for financial calculations leads to:

  • Rounding errors
  • Reconciliation drift
  • Bugs that only surface under load, scale, or edge cases

Use integers for minor units (e.g. cents) or proper decimal types. Never float.

LLMs Are Not Helping

Large language models like ChatGPT can now write your APIs, schemas, and database models.

But unless you’re careful, they’ll replicate the same bad patterns. Just faster:

  • amount: number with no context
  • float instead of decimal
  • JSON blobs without units, validation, or structure

LLMs autocomplete based on frequency, not correctness. Without strong engineering guardrails, you’re just mass-producing technical debt.

Databases and the Illusion of Structure

Even when your application code is well-typed, your database schema often isn’t. Relational databases may enforce column types (float, int, varchar), but they rarely capture meaning.

A column named amount might hold:

  • Gross or net values
  • In different currencies
  • As float or decimal
  • With or without tax
  • In cents or full units

Unless you enforce units and context explicitly, via naming conventions, type systems, or documentation, your schema becomes a silent source of ambiguity.

And in NoSQL databases like MongoDB, it gets worse.

Mongo stores everything as JSON-like documents. There’s no enforced schema unless you layer one on top. So teams end up storing arbitrary fields like:

{ "value": 99.99 }

What is value here? Dollars? Euros? A discount rate? A tax multiplier? Your system won’t tell you. It just stores whatever someone wrote last week.

And since Mongo supports flexible updates, it’s easy to gradually evolve documents into inconsistent messes. One document has "amount": "100", another has amount: 100.0, and a third has:

{
  "amount": { "value":    10000,
              "currency": "USD",
              "unit":    "cents" }
}

Now your query logic needs to branch, and bugs start to breed.

JSON: Ubiquitous and Ambiguous

JSON is everywhere: in APIs, logs, NoSQL databases, Kafka streams, and config files. But it doesn’t support real numbers.

JSON has one “number” type. There’s no distinction between:

  • Integer or float
  • Decimal or exponential
  • Currency, percent, duration, or count

This means:

  • "amount": 99.99 could be a float (in JS), a Decimal (in Python), or a BigDecimal (in Java)
  • Many serializers silently round, truncate, or lose precision
  • You can’t even tell if 99.99 is a valid business value without knowing the intended type

Worse, many languages parse JSON numbers as floats by default. That means even if you store exact values, your application might read them imprecisely.

COBOL—Structured but Not Safe

One of the oldest languages still in use, COBOL, often enforces more financial structure than many modern systems. Data is explicitly defined using `PIC` clauses:
05 AMOUNT         PIC 9(7)V99.  *> 9999999.99
05 CURRENCY-CODE  PIC X(3).     *> ISO-4217

This enforces field width, scale, and sometimes even units by convention. No wonder that many banks still use COBOL.

But COBOL lacks semantic typing; you get structure without meaning. Rigid, but still brittle when misunderstood.

You can’t trust what you can’t type.

What You Should Do Instead

Typed systems are better systems.

1. Make units explicit

To encode 100.00 Euro:

{
  "amount": 10000,
  "currency": "EUR",
  "minor_unit": true
}

2. Use the right types

  • Replace float with decimal
  • Use real bool and datetime types
  • Never store “truth” as "yes" or "Y"

3. Model the domain

  • Create types like VatRate, TransactionAmount, IsoTimestamp
  • Don’t let illegal states be representable

4. Validate at the edges

  • APIs should reject ambiguity, not propagate it
  • Fail fast when types or units are missing

Type Theory, Briefly

Types aren’t just for compilers. They encode meaning. They help humans and machines distinguish between:

  • 100 SEK vs 100%
  • net_total vs gross_total
  • sent_at vs due_date

Use types if you have them

In languages with strong type systems you can make these distinctions explicit:

// ISO 4217 currency codes (could use an enum)
type CurrencyCode = 'USD' | 'EUR' | 'GBP' | 'JPY';

interface Money {
    // always in the smallest currency unit
    // (e.g. cents, pence)
  amountMinor: number;
  currency: CurrencyCode;
}

When done well, the compiler helps you prevent bugs. Your system becomes self-documenting and harder to misuse.

Fake types if you don't

Even in dynamically typed languages, you can fake it:

  • Use structs, classes, or schema validators
  • Separate field names with intent (e.g. gross_eur_cents)
  • Build linter rules that enforce domain conventions

An example in Erlang

%%%-----------------------------------------------------------------
%%% money.hrl — record definition
%%%-----------------------------------------------------------------
-record(money, {
    amount_minor :: integer(),     % always in minor units (cents, öre)
    currency     :: currency()     % ISO-4217 atom
}).

-type currency() :: usd | eur | gbp | sek.
-export_type([currency/0, money/0]).
-type money()    :: #money{}.
%%%-----------------------------------------------------------------
%%% money.erl
%%%-----------------------------------------------------------------
-module(money).
-compile([export_all]).  % concise for the demo

-include("money.hrl").

-spec new(integer(), currency()) -> money().
new(AmountMinor, Cur) when
        is_integer(AmountMinor),
        AmountMinor >= 0,
        Cur =:= usd; Cur =:= eur; Cur =:= gbp; Cur =:= sek ->
    #money{amount_minor = AmountMinor, currency = Cur}.

-spec add(money(), money()) -> money().
add(#money{currency = C, amount_minor = A1},
    #money{currency = C, amount_minor = A2}) ->
    #money{currency = C, amount_minor = A1 + A2};
add(_, _) ->
    error({currency_mismatch}).

-spec format(money()) -> binary().
format(#money{amount_minor = Amt, currency = Cur}) ->
    Major = Amt div 100,
    Minor = Amt rem 100,
    lists:flatten(io_lib:format("~p.~2..0B ~p", [Major, Minor, Cur])).

Or at least pretend

Naming–as–Type, when you really have nothing else. If the tech stack is a bash script, SQL view, or low-code platform, fall back to semantic variable names:

ALTER TABLE invoices
ADD COLUMN gross_eur_cents BIGINT NOT NULL,
ADD COLUMN vat_rate_pct   NUMERIC(5,2) NOT NULL;

Now, in most modern SQL dialects you can use domains and composite types to achieve type safety.

-- 1. A domain for minor-unit amounts (cents, öre, pence …)
CREATE DOMAIN minor_money
AS bigint                               -- large enough for 9 223 372 036 854 775 807 cents
CHECK (VALUE >= 0);

-- 2. A domain for currency codes (exactly three uppercase A–Z letters)
CREATE DOMAIN currency_code
AS char(3)
CHECK (VALUE ~ '^[A-Z]{3}$');

-- 3. A composite type that glues the two together
CREATE TYPE money_value AS (
  amount_minor minor_money,
  currency     currency_code
);

-- 4. Example table using the new type
CREATE TABLE invoice_line (
  id           serial PRIMARY KEY,
  description  text NOT NULL,
  total_net    money_value NOT NULL,
  vat_amount   money_value NOT NULL,
  total_gross  money_value NOT NULL
);
INSERT INTO invoice_line (description, total_net, vat_amount, total_gross)
VALUES
  ('10 kg mozzarella',
   ROW(100000, 'EUR')::money_value,   -- €1 000.00 net
   ROW( 25000, 'EUR')::money_value,   -- €  250.00 VAT
   ROW(125000, 'EUR')::money_value);  -- €1 250.00 gross

The Real Problem

Modern languages make it easy to move fast without structure. That’s fine in early prototypes. But financial systems are long-lived. Without structure, every layer you build sits on sand.

The cost? Every ambiguous field becomes a future incident. Every vague type becomes a blocker to automation.

You can pay that cost now; through type safety and clear structure. Or you can pay it later; through failed audits, reconciliation hell, and broken trust.

Performance Myths

If you're concerned about throughput or message size: remember that using JSON or XML already dwarfs the overhead of a type‑safe add(Money) function.

Machine‑level addition is fast, but the bugs caused by mixing currencies or losing precision will cost far more.

Closing Thought

Finance runs on trust. Trust requires clarity. Clarity requires types.

If you’re still shipping Excel-style data into your APIs, databases, and ledgers then you’re shipping risk.

Typed data isn’t a nice-to-have. It’s a trust contract.


I barely scratched the surface here. If you’d like deeper material, whether a full book or a hands-on course, please let me know.


Need a second pair of eyes on your data contracts or payment architecture? Let’s talk → info@happihacking.se

- Happi


Happi Hacking AB
KIVRA: 556912-2707
106 31 Stockholm