<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: corpdigest</title>
    <description>The latest articles on DEV Community by corpdigest (@corpdigest).</description>
    <link>https://dev.to/corpdigest</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4064698%2Ff2577747-8acc-46b3-852b-b652e3547004.png</url>
      <title>DEV Community: corpdigest</title>
      <link>https://dev.to/corpdigest</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/corpdigest"/>
    <language>en</language>
    <item>
      <title>Revenue Is the Wrong Success Metric for a Distribution Business</title>
      <dc:creator>corpdigest</dc:creator>
      <pubDate>Mon, 17 Aug 2026 18:37:05 +0000</pubDate>
      <link>https://dev.to/corpdigest/revenue-is-the-wrong-success-metric-for-a-distribution-business-1415</link>
      <guid>https://dev.to/corpdigest/revenue-is-the-wrong-success-metric-for-a-distribution-business-1415</guid>
      <description>&lt;p&gt;If you rank companies by revenue, McKesson shows up near the very top — north of $300B a year, bigger than most household-name tech giants. If you rank them by net margin, it nearly disappears: the margin is well under 1%. Both numbers are correct, and storing only one of them in your company-data schema will mislead every downstream comparison you build.&lt;/p&gt;

&lt;p&gt;The issue is that revenue means completely different things depending on the business model behind it. For a software company, revenue is close to value captured. For a pure distributor like McKesson — which moves pharmaceuticals from manufacturers to pharmacies — revenue is mostly pass-through: the cost of the drugs flows straight through the income statement. The company's actual economics live in the thin spread it keeps, not the enormous number at the top of the P&amp;amp;L.&lt;/p&gt;

&lt;p&gt;So a &lt;code&gt;revenue&lt;/code&gt; column, used as a proxy for "size" or "success" across a mixed dataset, systematically overweights distributors, wholesalers, and retailers against asset-light businesses that capture far more per dollar of revenue. A "largest companies" ranking sorted on revenue alone will tell you McKesson is more significant than companies with a tenth of its revenue and ten times its profit.&lt;/p&gt;

&lt;p&gt;The schema fix is to stop treating revenue as a scalar success metric and store the fields that let you compute the one that matters for the question you're asking: &lt;code&gt;gross_profit&lt;/code&gt;, &lt;code&gt;net_income&lt;/code&gt;, and ideally &lt;code&gt;revenue_model&lt;/code&gt; (pass-through vs. value-capture) as an explicit category. Then "rank by size" becomes an intentional choice — revenue, gross profit dollars, or margin — rather than an accident of which column was handy.&lt;/p&gt;

&lt;p&gt;I dug into this while writing up &lt;a href="https://corpdigest.com/company/mckesson/business-model" rel="noopener noreferrer"&gt;how McKesson actually makes money&lt;/a&gt; — the thin-margin-at-massive-scale model is the clearest example I've found of why a single revenue figure lies.&lt;/p&gt;

&lt;p&gt;How do you handle this in your own schemas — a &lt;code&gt;revenue_model&lt;/code&gt; enum, a computed margin column, or something else?&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>business</category>
      <category>ai</category>
      <category>data</category>
    </item>
    <item>
      <title>ARPU Is the Wrong Shape for a Multi-Surface Ad Business</title>
      <dc:creator>corpdigest</dc:creator>
      <pubDate>Sun, 16 Aug 2026 02:22:23 +0000</pubDate>
      <link>https://dev.to/corpdigest/arpu-is-the-wrong-shape-for-a-multi-surface-ad-business-4com</link>
      <guid>https://dev.to/corpdigest/arpu-is-the-wrong-shape-for-a-multi-surface-ad-business-4com</guid>
      <description>&lt;p&gt;A lot of company-data schemas have an &lt;code&gt;arpu&lt;/code&gt; field: total revenue divided by monthly active users, stored as one number per company per period. It's a fine number to report. It's a bad number to model a business on, and Meta is the clearest example of why.&lt;/p&gt;

&lt;p&gt;Meta's advertising business doesn't monetize "a user." It monetizes a user on a surface, at a data density, in a geography. A single global ARPU figure averages away exactly the three variables that explain the revenue. North American ARPU runs several multiples higher than Asia-Pacific ARPU on the same platform, not because the ads are different, but because advertiser willingness-to-pay per impression tracks disposable income and ad-market maturity, not usage. Collapse that into one column and you've thrown away the variable that actually explains the business model.&lt;/p&gt;

&lt;p&gt;The practical schema fix is unglamorous: stop treating ARPU as a stored fact and start treating it as a derived view over (revenue, users, dimension) tuples, where dimension is at minimum geography and ideally product surface (feed ads vs. Reels vs. click-to-WhatsApp). If your table can't answer "ARPU for what surface, in what region" it can't actually explain how the money is made, only how much of it there was.&lt;/p&gt;

&lt;p&gt;I ended up writing this up after digging into how &lt;a href="https://corpdigest.com/company/meta/business-model" rel="noopener noreferrer"&gt;Meta's business model&lt;/a&gt; actually breaks down by segment for a data project, worth a look if you want a concrete example of what the disaggregated numbers look like in practice.&lt;/p&gt;

&lt;p&gt;Curious how others have modeled this. Do you store per-surface ARPU as a materialized column, or compute it at query time from raw revenue/user facts?&lt;/p&gt;

</description>
      <category>data</category>
      <category>analytics</category>
      <category>business</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your acquisitions table needs a status field, not a boolean</title>
      <dc:creator>corpdigest</dc:creator>
      <pubDate>Fri, 14 Aug 2026 18:57:46 +0000</pubDate>
      <link>https://dev.to/corpdigest/your-acquisitions-table-needs-a-status-field-not-a-boolean-1gck</link>
      <guid>https://dev.to/corpdigest/your-acquisitions-table-needs-a-status-field-not-a-boolean-1gck</guid>
      <description>&lt;p&gt;Most schemas model corporate acquisitions as a join table: &lt;code&gt;acquirer_id&lt;/code&gt;, &lt;code&gt;target_id&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;closed_date&lt;/code&gt;. Some add &lt;code&gt;is_completed BOOLEAN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That boolean is where the model starts lying to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four states a boolean cannot hold
&lt;/h2&gt;

&lt;p&gt;An acquisition is not binary. Working through several hundred company profiles, the deals refuse to sit in two buckets:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Announced but not closed.&lt;/strong&gt; In July 2026 Uber announced a voluntary takeover offer for Delivery Hero, around $14.8 billion, with closing expected in the second half of 2027 subject to conditions and regulatory approval. If your pipeline writes that row with &lt;code&gt;is_completed = false&lt;/code&gt;, it will be indistinguishable from a deal that collapsed. If it writes &lt;code&gt;true&lt;/code&gt;, you have just booked $14.8B of M&amp;amp;A that has not happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closed, then unwound.&lt;/strong&gt; Uber bought Drizly in 2021 for about $1.1 billion, ran it as a standalone service, and shut it down in 2024, folding alcohol ordering into Uber Eats. The acquisition completed. The entity no longer exists. Both facts are true and a boolean holds neither well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closed, then divested.&lt;/strong&gt; McDonald's took a stake in Chipotle in 1998 and had fully divested by 2006. Financially it was one of the best investments in restaurant history. Strategically it was abandoned. "Completed" is accurate and useless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closed with partial disposal.&lt;/strong&gt; The Uber–Delivery Hero offer earmarks 14 overlapping markets for separate sale to SSW Partners. The deal, if it closes, is simultaneously an acquisition and a divestiture.&lt;/p&gt;

&lt;h2&gt;
  
  
  A state field that survives contact
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;acquisition_status&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'announced'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- disclosed, not closed&lt;/span&gt;
  &lt;span class="s1"&gt;'terminated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- announced, then abandoned&lt;/span&gt;
  &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- closed, target operating&lt;/span&gt;
  &lt;span class="s1"&gt;'absorbed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- closed, brand retired into acquirer&lt;/span&gt;
  &lt;span class="s1"&gt;'shuttered'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- closed, operations discontinued&lt;/span&gt;
  &lt;span class="s1"&gt;'divested'&lt;/span&gt;        &lt;span class="c1"&gt;-- closed, later sold on&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things this buys you immediately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;announced&lt;/code&gt; rows can be excluded from any "total M&amp;amp;A spend" aggregate with a single predicate, instead of relying on every downstream consumer to remember.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shuttered&lt;/code&gt; and &lt;code&gt;divested&lt;/code&gt; become queryable. "Which acquirers shut down what they bought?" is a genuinely interesting question and a boolean makes it unanswerable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Add &lt;code&gt;status_as_of DATE&lt;/code&gt; alongside it. Status is a point-in-time claim, not a permanent property, and without a date you cannot tell a stale row from a current one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counting trap
&lt;/h2&gt;

&lt;p&gt;The related mistake: treating a row count as a fact about the world.&lt;/p&gt;

&lt;p&gt;If a profile documents five acquisitions, that means five acquisitions are documented. It does not mean the company made five. Undisclosed deals, asset purchases and acqui-hires routinely never surface. Name the column &lt;code&gt;acquisitions_documented&lt;/code&gt; rather than &lt;code&gt;acquisition_count&lt;/code&gt; and the ambiguity disappears at the point of use, which is where it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked example
&lt;/h2&gt;

&lt;p&gt;If you want a deal-by-deal record with stated purpose and actual outcome for each — including the pending Delivery Hero offer explicitly flagged as pending rather than counted — the &lt;a href="https://corpdigest.com/company/uber/acquisitions" rel="noopener noreferrer"&gt;Uber acquisition history&lt;/a&gt; is laid out that way.&lt;/p&gt;

&lt;p&gt;The general principle is boring and keeps paying: model the states your data actually occupies, not the two states that were convenient when the table was created.&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Your companies table is lying to you about when things happened</title>
      <dc:creator>corpdigest</dc:creator>
      <pubDate>Sat, 08 Aug 2026 19:02:36 +0000</pubDate>
      <link>https://dev.to/corpdigest/your-companies-table-is-lying-to-you-about-when-things-happened-ge3</link>
      <guid>https://dev.to/corpdigest/your-companies-table-is-lying-to-you-about-when-things-happened-ge3</guid>
      <description>&lt;p&gt;Every company dataset I have worked with stores history as scalar columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;founded_year&lt;/span&gt;   &lt;span class="nb"&gt;INT&lt;/span&gt;
&lt;span class="n"&gt;ceo_name&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="n"&gt;headquarters&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="n"&gt;revenue&lt;/span&gt;        &lt;span class="nb"&gt;BIGINT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those is a point-in-time fact pretending to be a permanent one. The moment you need to answer "who was in charge when this happened", the schema has already thrown the answer away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure is invisible until you query across time
&lt;/h2&gt;

&lt;p&gt;Take Intel. Founded 18 July 1968 by Gordon Moore and Robert Noyce in Mountain View. If your row says &lt;code&gt;founded_year = 1968&lt;/code&gt; and &lt;code&gt;ceo_name = &amp;lt;whoever it is today&amp;gt;&lt;/code&gt;, you have compressed nearly six decades into two fields that were never simultaneously true.&lt;/p&gt;

&lt;p&gt;Now try to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who was running the company when the 4004 shipped in 1971?&lt;/li&gt;
&lt;li&gt;What was the headquarters when the IBM PC design win landed in 1981?&lt;/li&gt;
&lt;li&gt;Which leadership team owned the process-node slippage that Apple Silicon exposed in 2020?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are answerable. Not because the data is missing, but because the schema has no time axis.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is an event table, not more nullable columns
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;company_event&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;company_id&lt;/span&gt;    &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;   &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- 'ceo_start' | 'hq_move' | 'rename' | 'launch'&lt;/span&gt;
  &lt;span class="n"&gt;valid_from&lt;/span&gt;    &lt;span class="nb"&gt;date&lt;/span&gt;   &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;valid_to&lt;/span&gt;      &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;-- null means still current&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;       &lt;span class="n"&gt;jsonb&lt;/span&gt;  &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;source_url&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;   &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things this buys you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;valid_to IS NULL&lt;/code&gt; means current.&lt;/strong&gt; Your "current CEO" lookup becomes a view rather than a column somebody has to remember to update. Staleness stops being a silent failure and becomes a missing row you can detect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;source_url&lt;/code&gt; sits on the row, not the parent.&lt;/strong&gt; When a specific figure gets challenged you can point at the specific filing that supports that specific claim. A single &lt;code&gt;source&lt;/code&gt; column on the company row cannot do that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Corrections stop being destructive.&lt;/strong&gt; Updating a scalar &lt;code&gt;ceo_name&lt;/code&gt; destroys the previous value. Inserting an event with a new &lt;code&gt;valid_from&lt;/code&gt; preserves it, which means you can reconstruct what you believed and when.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually bit me
&lt;/h2&gt;

&lt;p&gt;The schema is the easy half. The hard half is that most sources do not give you a clean &lt;code&gt;valid_from&lt;/code&gt;. Press releases say "effective immediately". Filings say "during fiscal 2024". Wikipedia says "1968". So you need to store how precise the date is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;valid_from&lt;/span&gt;            &lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="n"&gt;valid_from_precision&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;  &lt;span class="c1"&gt;-- 'day' | 'month' | 'quarter' | 'year'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without that column you will silently invent a day-level date no source supports, and something downstream will treat it as exact. I have done this. It is very annoying to unpick later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked example
&lt;/h2&gt;

&lt;p&gt;If you want to see the shape rendered as narrative rather than rows, the &lt;a href="https://corpdigest.com/company/intel/company-history" rel="noopener noreferrer"&gt;Intel company history&lt;/a&gt; page is built this way -- founding, the 4004, the IBM PC standardisation and the Apple Silicon transition as discrete dated events with sources attached, rather than one prose blob with a &lt;code&gt;founded_year&lt;/code&gt; bolted on.&lt;/p&gt;

&lt;p&gt;The rule I have settled on: if a fact about a company can change, it is an event with a start date and a source. It is not a column.&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Banks Break Your Company-Data Schema, and the Fix Is Not Another Nullable Column</title>
      <dc:creator>corpdigest</dc:creator>
      <pubDate>Thu, 06 Aug 2026 17:41:28 +0000</pubDate>
      <link>https://dev.to/corpdigest/banks-break-your-company-data-schema-and-the-fix-is-not-another-nullable-column-25bn</link>
      <guid>https://dev.to/corpdigest/banks-break-your-company-data-schema-and-the-fix-is-not-another-nullable-column-25bn</guid>
      <description>&lt;p&gt;If you have ever modelled companies in a database, you have probably started with a schema that looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;company&lt;/span&gt;
  &lt;span class="s"&gt;id&lt;/span&gt;
  &lt;span class="s"&gt;name&lt;/span&gt;
  &lt;span class="s"&gt;founded_year&lt;/span&gt;
  &lt;span class="s"&gt;headquarters&lt;/span&gt;
  &lt;span class="s"&gt;employee_count&lt;/span&gt;
  &lt;span class="s"&gt;products[]&lt;/span&gt;
  &lt;span class="s"&gt;revenue_streams[]&lt;/span&gt;
  &lt;span class="s"&gt;competitors[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works fine for a manufacturer or a SaaS vendor. You can reason from products to revenue, and &lt;code&gt;revenue_streams&lt;/code&gt; maps onto things a customer actually buys.&lt;/p&gt;

&lt;p&gt;Then you add a bank, and the model quietly stops describing reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

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

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

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

&lt;h2&gt;
  
  
  Why a nullable column does not fix it
&lt;/h2&gt;

&lt;p&gt;The tempting patch is to add &lt;code&gt;is_financial_institution&lt;/code&gt; and a few nullable fields. That gets you through the migration and creates two failure modes:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;which businesses to keep&lt;/em&gt; rather than which products to ship.&lt;/p&gt;

&lt;p&gt;If you want to see what fields a bank profile actually needs, the &lt;a href="https://corpdigest.com/company/deutsche-bank/business-model" rel="noopener noreferrer"&gt;Deutsche Bank business model&lt;/a&gt; page is a reasonable reference for the shape of the data: divisional structure and revenue logic rather than a product list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently next time
&lt;/h2&gt;

&lt;p&gt;Start with the variant. Write down two or three company archetypes before you write the first migration:&lt;/p&gt;

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

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

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

</description>
      <category>database</category>
      <category>architecture</category>
    </item>
    <item>
      <title>foundedYear Is the Wrong Field for Company Data</title>
      <dc:creator>corpdigest</dc:creator>
      <pubDate>Wed, 05 Aug 2026 18:48:46 +0000</pubDate>
      <link>https://dev.to/corpdigest/foundedyear-is-the-wrong-field-for-company-data-37n1</link>
      <guid>https://dev.to/corpdigest/foundedyear-is-the-wrong-field-for-company-data-37n1</guid>
      <description>&lt;p&gt;Almost every company dataset stores founding as a single integer. It is the first field you add and the first one that turns out to be a lie.&lt;/p&gt;

&lt;p&gt;Take Visa Inc. What year was it founded?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1958, when Bank of America launched the BankAmericard programme&lt;/li&gt;
&lt;li&gt;The early 1970s, when the programme was reorganised into a member-owned network under Dee Hock&lt;/li&gt;
&lt;li&gt;1976, when it took the Visa name&lt;/li&gt;
&lt;li&gt;2007-2008, when Visa Inc. was incorporated as a stock corporation and went public&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four are defensible. Your integer field forces you to pick one and silently discard the other three. Worse, whichever you pick becomes unfalsifiable downstream, because the reasoning is not stored anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model entity events, not a founding year
&lt;/h2&gt;

&lt;p&gt;Keep the integer for display, but make it derived rather than authoritative. Store the events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"visa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"displayFoundedYear"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1958&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entityEvents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1958&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"program_launch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BankAmericard"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1976&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rename"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Visa"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2008&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ipo"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things get easier immediately. Conflicting sources stop being bugs and become different event types. "Companies founded before 1970" becomes a question you can answer precisely instead of approximately. And when someone challenges a date, you can show them why you chose it.&lt;/p&gt;

&lt;p&gt;The rendered version of that record, for reference: &lt;a href="https://corpdigest.com/company/visa" rel="noopener noreferrer"&gt;Visa company data&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Banks, insurers and anything that has been through a demutualisation will break the same way. If your schema has a single founding integer, pick your oldest company and try to defend the number. That exercise is the whole argument.&lt;/p&gt;

</description>
      <category>database</category>
      <category>api</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Modelling Corporate Acquisition History as a Graph, Not a List</title>
      <dc:creator>corpdigest</dc:creator>
      <pubDate>Wed, 05 Aug 2026 17:59:41 +0000</pubDate>
      <link>https://dev.to/corpdigest/modelling-corporate-acquisition-history-as-a-graph-not-a-list-1clp</link>
      <guid>https://dev.to/corpdigest/modelling-corporate-acquisition-history-as-a-graph-not-a-list-1clp</guid>
      <description>&lt;p&gt;Most acquisition data on the web is stored as a flat list: acquirer, target, date, price. That shape is easy to render in a table and nearly useless for analysis, because it throws away the thing you actually want - structure.&lt;/p&gt;

&lt;p&gt;Acquisitions are a graph. Nodes are entities, edges are transactions with a direction and a timestamp. Once you model it that way, a set of queries opens up that a flat table cannot answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chains.&lt;/strong&gt; Company A buys B; B had previously bought C. The IP you care about moved twice. A flat list shows two unrelated rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Divestments.&lt;/strong&gt; An entity leaves the graph and re-enters elsewhere. Spin-offs are edges too, and omitting them makes portfolios look monotonically expanding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clustering by category.&lt;/strong&gt; Group edges by target category and the acquirer's strategy becomes visible as a shape rather than a narrative.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pharmaceutical companies are the clearest test case for this, because their acquisition activity is pipeline acquisition. Buying a company is often buying one molecule at a specific trial phase. Novartis AG is a useful example - the company was itself formed in 1996 by the merger of Ciba-Geigy and Sandoz, so its own root node is a merger, and its subsequent activity includes both acquisitions and significant divestments as it moved away from generics.&lt;/p&gt;

&lt;p&gt;A schema that survives contact with real data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"acquirer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"novartis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example-bio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"announced"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-03-11"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"closed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-07-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acquisition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oncology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"disclosed_value_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two design notes.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;announced&lt;/code&gt; and &lt;code&gt;closed&lt;/code&gt; separate. Deals fall through, and collapsing the two dates silently rewrites history.&lt;/p&gt;

&lt;p&gt;Make &lt;code&gt;disclosed_value_usd&lt;/code&gt; explicitly nullable rather than defaulting to zero. "Undisclosed" and "zero" are very different facts, and &lt;code&gt;0&lt;/code&gt; will quietly poison every aggregate you compute.&lt;/p&gt;

&lt;p&gt;For a worked example of the raw material, the &lt;a href="https://corpdigest.com/company/novartis/acquisitions" rel="noopener noreferrer"&gt;Novartis acquisition history&lt;/a&gt; is laid out chronologically, which is a reasonable input format before you normalise it into edges.&lt;/p&gt;

&lt;p&gt;Once the graph exists, the interesting queries are the ones about time: how long between a therapeutic area's first acquisition and its second, and does that interval shorten when a strategy is working?&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>data</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
