DEV Community

corpdigest
corpdigest

Posted on

Banks Break Your Company-Data Schema, and the Fix Is Not Another Nullable Column

If you have ever modelled companies in a database, you have probably started with a schema that looks something like this:

company
  id
  name
  founded_year
  headquarters
  employee_count
  products[]
  revenue_streams[]
  competitors[]
Enter fullscreen mode Exit fullscreen mode

It works fine for a manufacturer or a SaaS vendor. You can reason from products to revenue, and revenue_streams maps onto things a customer actually buys.

Then you add a bank, and the model quietly stops describing reality.

The problem

For a product company, the business model is downstream of what it sells. For a bank, the business model is the balance sheet. The question "what does this company sell?" has no clean answer. What matters instead is:

  • which activities consume regulatory capital
  • which income is fee-based and recurring versus market-dependent and volatile
  • how the divisions are structured, because divisions are the real unit of analysis
  • where the funding comes from, because funding mix drives the entire risk profile

None of those fit in a products[] array. If you force them in, you end up with rows like {name: "Investment Banking", type: "product"}, which is a category error your queries will inherit forever.

Why a nullable column does not fix it

The tempting patch is to add is_financial_institution and a few nullable fields. That gets you through the migration and creates two failure modes:

  1. Aggregations lie. Any AVG(revenue_per_product) style query silently includes companies where the denominator is meaningless.
  2. The nulls spread. Insurers, REITs, asset managers and exchanges each need a slightly different set, so you keep adding columns that are null for 95% of rows.

The honest modelling answer is that "business model" is not one shape. It is a discriminated union, and financial institutions are a distinct variant with their own required fields. Whether you implement that as separate tables, a JSONB payload with a schema tag, or single-table inheritance matters less than admitting the variant exists.

A concrete example

Deutsche Bank is a useful test case because its structure is legible and its history explains it. It was founded in Frankfurt in 1870 by Adelbert Delbrück and Wilhelm von Philipsborn for a specific reason: German merchants depended on British banks to finance international trade, and the new institution existed to do that financing domestically.

That origin still shows up in the divisional structure 155 years later. The bank has always leaned corporate and cross-border rather than domestic-retail-deposit, which is why the 1989 acquisition of Morgan Grenfell and the later global-markets build-out read as continuations of the founding logic rather than departures from it. Roughly 90,000 employees, and a restructuring under Christian Sewing that was fundamentally a question of which businesses to keep rather than which products to ship.

If you want to see what fields a bank profile actually needs, the Deutsche Bank business model page is a reasonable reference for the shape of the data: divisional structure and revenue logic rather than a product list.

What I would do differently next time

Start with the variant. Write down two or three company archetypes before you write the first migration:

  • Product company — products, unit economics, revenue streams
  • Financial institution — divisions, capital consumption, funding mix, fee vs market income
  • Marketplace / platform — two-sided supply and demand, take rate, liquidity

You will discover more archetypes later. But starting with one and bolting on the rest is how you end up with a companies table that has 140 columns and no one willing to touch it.

If you have modelled this differently, I would like to hear it, particularly how you handled conglomerates that span more than one archetype.

Top comments (0)