Before building a reference catalog, check whether the attribute reaches the computation

·5 mins

A tax engine for investment accounts has to put every instrument into a category, because the category decides the rules. A real share sold after the holding period can be exempt. A CFD on the same stock is a derivative and is taxed no matter how long you hold it. Brokers don’t make this easy. One reports CFDs under “stocks”. A position can be a fractional share or a whole one. An ETF’s ISIN doesn’t say whether the fund pays out dividends or reinvests them.

During the design session, the coding agent recommended a central instrument catalog: ISIN → {category, asset class, accumulating/distributing, derivative, fractional, …}, seeded from a market data provider and maintained over time. Per-broker adapters would propose a category and reconcile it against the catalog, and anything unresolved would go to a review queue. It argued that the accumulating/distributing flag in particular had to come from the catalog.

I rejected it in two sentences:

If we get the dividend, we have to deal with it. If the fund gets the dividend, it’s non-taxable for the individual.

The attribute decides whether an event exists

A distributing fund pays the investor a dividend, and the broker reports it as a dividend event, which is taxed. An accumulating fund keeps the dividend inside the fund. The investor receives nothing, and the broker reports nothing, so nothing is taxed until the units are sold, and the sale is an ordinary sell event.

The flag the catalog was meant to supply only decides whether an event exists, and the transaction ledger already records whether one did. Removing the flag from the catalog changes no input to the engine. It was also the catalog’s main justification.

The reusable check: for every attribute you are about to maintain as reference data, flip its value and ask which computation input changes.

AttributeWhat changes if it flipsAlready in the events?Source
Accumulating / distributingWhether a dividend event existsYes: the event is present or absentNone needed
CFD / real shareThe rules applied to the same sell eventNoThe broker’s own export (instrument type, leverage)
Fractional / whole shareWhether an exemption is arguableNoThe broker’s idiom
Crypto / token / derivative on cryptoWhich rules and caps applyNoThe broker’s idiom

The remaining attributes do change the computation. All of them can be read from the broker that produced the record, so classification belongs in the per-broker adapter. The adapter already parses that broker’s export and knows its quirks. A global catalog would only reconcile against them afterward.

Classify where the source idiom lives, and refuse to guess

The adapter for the broker that labels CFDs as stocks classifies by the type and leverage columns of its own export:

function classify(type: unknown, leverage: unknown): Category | { explanation: string } {
  const kind = header(type);
  if (/cfd|contract for difference/.test(kind)) return "derivative";
  const multiplier = Number(text(leverage).replace(/x$/i, ""));
  if (!kind || !Number.isFinite(multiplier) || multiplier < 1) return { explanation: "Type or Leverage is missing or invalid." };
  if (kind.includes("crypto")) {
    return multiplier === 1 ? "crypto" : { explanation: `Crypto Type conflicts with leveraged position (${text(leverage)}).` };
  }
  if (multiplier > 1 && /(stock|etf|real|cfd|contract)/.test(kind)) return "derivative";
  if (multiplier === 1 && /(stock|etf|real|share|security)/.test(kind)) return "security";
  return { explanation: `Type ${text(type)} and Leverage ${text(leverage)} cannot be classified.` };
}

No default branch picks the most likely category. A row the rules cannot classify becomes a review item (AMBIGUOUS_CATEGORY with the raw row reference) and stays out of the tax computation. A misclassified instrument produces a wrong filing with no warning sign, which is worse than an incomplete one.

The ISIN and ticker stay on the record for cross-checking and audit, but nothing uses them to decide a category.

What dropping the catalog removed

  • A licensed data feed and the work of keeping up with new listings, delistings, and corrections.
  • A reconciliation layer between adapter output and catalog values, plus the conflict UI that goes with it.
  • A global table keyed by ISIN that every user’s trades would look up. In this design, real instrument identity is visible only inside the adapter at ingestion, and the stored record carries a pseudonymous ID. That design is described in When the server must compute on confidential data, split the data by who may read it .

The cost moved into the adapters: classification is now only as correct as each adapter. Every adapter has its own test suite for its broker’s export format, and adding a broker means writing its classification rules and tests.

Caveat: some tax systems tax events that never happened

The shortcut holds where tax follows money the investor actually receives. Some tax systems tax deemed income on fund holdings that paid nothing out. Germany’s Vorabpauschale, UK reporting funds’ excess reportable income and the US PFIC regime are examples. There, the accumulating flag produces an income line that no event carries, and you need the attribute from somewhere. That is exactly the case the flip test catches: the computation input changes even though no event changes.

Takeaway

Before you maintain reference data for an attribute, flip it and see which input of your computation changes. If the only effect is whether an event happens, and your event stream already records that, you don’t need the attribute. If an input does change, get the value from whoever produced the record, and send anything it can’t classify to review.

The agent’s recommendation was internally consistent and sounded responsible. It took someone who knew where the taxable event comes from to see that the attribute changed nothing. When an agent proposes reference data, run the flip test on every attribute before accepting it.