When Business Rules Turn Into Silent Data Corruption: Lessons From Building a Tax Engine
Admin User
Author
I spent three hours last week tracking down why a client's financial report was off by exactly one transaction. Not a calculation error—the number was simply missing from the dataset. One sale, gone. The kind of bug that makes you question your data pipeline at 2 AM. Reading about how Diego built Balance's tax system hit me hard because I've lived this exact nightmare: complex business logic that seems simple until you try to automate it, then suddenly you're dealing with transaction ordering, state mutations, and edge cases that don't exist in the happy path.
The real kicker? These bugs don't fail loudly. They fail silently, corrupting your entire dataset in ways you won't notice until someone's accountant flags a discrepancy three months later.
The Deceptive Simplicity of "Just Sum The Numbers"
When a product manager says "we just need to calculate taxes based on these three rules," your instinct is to reach for a few if-statements and call it done. But tax systems—whether Brazilian capital gains tax or anything involving regulatory compliance—aren't simple. They're intricate. And there's a massive difference.
Diego's example is perfect: the R$20,000 monthly exemption applies only to common stocks, not REITs or ETFs. Most developers would hardcode this as a conditional somewhere. Most investors, burned by penalties, learn this the hard way. The system needs to understand that concept, not just implement the rule.
What gets me is how this mirrors every complex domain problem I've encountered. Billing systems where one customer type gets different discount rules. Subscription logic where cancellation timing changes everything. Inventory systems where the same product behaves differently across regions. The pattern is identical: surface-level rules that explode into nightmare scenarios when you try to make them deterministic.
The Cost Basis Trap: Why You Can't Cache Your Way Out
Here's where most developers fail: they cache the average cost and update it on every transaction. Sounds reasonable. It's also completely wrong for this domain.
The genius move is recognizing that cost basis should be reconstructed, not stored. Every time you need it, you replay the entire transaction history in chronological order. Yes, it's more expensive computationally. But it's correct, and it's auditable.
The second-order insight Diego mentions is even more important: a sale doesn't change the average cost. It reduces quantity, nothing more. I've seen systems corrupt their entire dataset because they recalculated average cost after every sale. The history doesn't just break—it breaks silently. Month three's calculations are wrong because month two unknowingly started with corrupted state.
And the sorting detail—by date and created_at—isn't pedantic. It's the difference between a system that sometimes works and one that always works. Bulk imports can insert transactions out of chronological order. Without the tiebreaker, same-day transactions fail unpredictably.
# The right way: replay history deterministically
def recalculate_avg_cost(portfolio, ticker: str):
txns = (Transaction.objects
.filter(portfolio=portfolio, ticker=ticker)
.order_by('data', 'created_at')) # The tiebreaker matters
qty = Decimal('0')
total_cost = Decimal('0')
for t in txns:
if t.tipo == 'COMPRA':
total_cost += t.quantidade * t.preco_unitario + t.custo_operacional
qty += t.quantidade
elif t.tipo == 'VENDA' and qty > 0:
avg = total_cost / qty
total_cost -= t.quantidade * avg
qty -= t.quantidade
# Update once, after full replay
return (total_cost / qty) if qty > 0 else Decimal('0')
This isn't just code. It's a philosophy: derive state from history, don't mutate it incrementally.
Separate Loss Pools: The Rule That Breaks Everything
The monthly assessment introduces another gotcha: stock losses can't offset REIT gains. They're separate pools carrying forward independently. I can absolutely see a developer implementing this wrong, then wondering why December's numbers don't match the tax form.
My Take
What impresses me most about Diego's approach is the commitment to modeling concepts instead of piling on conditionals. Loss pools are first-class objects. Asset types aren't strings scattered through the codebase—they're determinate by a consistent rule (ticker ends in '11'). Average cost is something you derive, not store.
This is the bridge between "it works on my machine" and "it survives code review and production." It's the difference between code that passes tests and code that's correct.
The other thing: the built-in accountability. Every report carries a disclaimer: this doesn't replace an accountant. That's maturity. You're automating the mechanical part—the summing, the separating, the remembering—but you're not pretending to replace human judgment.
What's your experience with domain-specific logic? Have you hit these silent corruption bugs, or found patterns that prevent them?
Source: This post was inspired by "Brazilian capital gains tax as a Django service" by Dev.to. Read the original article