How GimBooks Served 3M Users Without Breaking GST Logic
For: A Series A fintech founder or CTO whose accounting or compliance SaaS is working at 50K users but whose GST reconciliation logic is silently producing wrong outputs at edge cases — and who suspects the architecture will not survive 10x growth without a rewrite they cannot afford
If your GST reconciliation works for 90% of invoices but silently produces wrong outputs on composite dealers, RCM entries, or inter-state B2C — the fix is almost never better validation. It is a data model change. GimBooks scaled to millions of Indian SMB users because the compliance layer was eventually rebuilt as an explicit ledger of supply-type state transitions, not a library of tax formulas. Every team that models GST as if place_of_supply != state then IGST else CGST+SGST ships correct code for the demo and wrong code for reality.
This is the engineering walkthrough — what broke, what we tried, what actually worked, and the lessons that transfer to any compliance SaaS pushing past its first million users.
The starting point: a working product with quiet failures
GimBooks is an accounting and invoicing app for Indian micro-businesses — kirana stores, small manufacturers, service providers, freelancers. Most users file GST themselves or with a part-time CA. The product had to make GST filing feel like WhatsApp, not like Tally.
Early architecture was what every fintech SaaS starts with:
- A monolithic backend with an
invoicestable - Tax calculation as a set of pure functions — inputs go in, CGST/SGST/IGST come out
- A GSTR-1 / GSTR-3B export that read the invoice table, grouped by tax slab, and produced the JSON the GSTN portal accepts
This works. It works for a long time. It worked at 50,000 users. Then support tickets started clustering into patterns that had nothing in common on the surface:
- A composite dealer downgraded from regular scheme mid-quarter — their filings were now half wrong, half right
- A supplier issued a credit note in July against an invoice from April, and the reconciliation showed two conflicting tax liabilities
- An unregistered buyer in a different state was being charged IGST correctly, but the GSTR-1 export was putting them in the wrong B2C table
- Reverse charge (RCM) transactions were being counted twice — once as an expense and once as an output tax
- An e-commerce operator collected TCS on a sale, and the seller's books had no idea how to reconcile it back
Each ticket looked like a bug. It was not. It was the same bug wearing different clothes.
What we tried first (and why it didn't work)
Attempt 1: More validation
The first instinct — the one every team has — was to add validation rules. If a user is a composite dealer, block them from adding CGST/SGST. If an invoice is RCM, flag it. If place-of-supply is different from the state of registration, force IGST.
This produced two failures. First, the validation surface grew faster than we could test it — each new GST notification from CBIC added constraints that interacted with existing constraints in ways no one had reasoned through. Second, and worse, validation blocked users from entering legitimate edge-case transactions the tax code actually permitted. The product got more annoying without getting more correct.
Attempt 2: A rules engine
Next attempt: pull the tax logic out of application code into a declarative rules engine. Something along the lines of Drools, but lighter. Rules could be authored by the CA on the team, versioned, and hot-swapped when a new notification dropped.
This felt like the right abstraction for about six weeks. Then we hit the actual problem. A rules engine assumes the input is a well-defined fact and the output is a decision. GST is not that. GST is a temporal system — the correct tax treatment of an invoice today depends on the state of the taxpayer, the state of the counterparty, and the state of the transaction chain over time. A credit note in October against an invoice from July has to reverse the exact tax treatment used in July, even if the rules changed in September.
Rules engines are stateless. GST is stateful. We had abstracted the wrong axis.
Attempt 3: Patch the reconciler
Third attempt was the ugliest and, honestly, the one most teams get stuck in for years. Leave the calculation code alone. Write a reconciliation layer on top that catches known-bad patterns after the fact and either auto-fixes or flags them for review.
This scaled linearly with edge cases and squared with their interactions. Every new patch introduced two new patches somewhere else. The reconciler became the largest module in the codebase and the most feared during releases.
The architectural call: GST as a state machine over a supply ledger
The reframe that unlocked scale was this: a GST-compliant invoice is not a row. It is the current projection of a sequence of events on a supply relationship between two parties.
Concretely, we moved to a model with three primitives:
- Party state — a versioned record of every counterparty and the taxpayer themselves. Registration type (regular, composite, unregistered, SEZ, e-commerce operator), state of registration, effective dates for every change. If a party switched from composite to regular on August 14, both facts are stored and both are queryable by date.
- Supply events — immutable, append-only events for every taxable act: invoice issued, credit note issued, debit note issued, payment received against advance, RCM liability recognized, TCS collected. Each event carries the party state IDs at the moment it occurred.
- Supply-type state machine — an explicit finite state machine per supply relationship. A B2B supply between two registered dealers in different states is one state. A B2C supply from a regular dealer to an unregistered buyer is another. A reverse charge supply is another. Transitions between states are events, not recalculations.
The tax computation became a projection: given the current state of the supply and the event being applied, what tax entries does this generate? The GSTR-1 export became a fold over the event log for the filing period, filtered by state.
Two consequences fell out of this that we did not anticipate at design time.
First, the "credit note in October against an invoice from July" problem solved itself. The credit note event carries a reference to the original invoice event, which carries the party states as they were in July. The reversal is automatically correct even if rules changed in September, because the state machine transition is defined against the historical state, not the current one.
Second, when CBIC issued a new notification — and they issue them constantly — the change was almost always a new state or a new transition, not a rewrite of existing math. Adding a state is safe. Rewriting math is not. The rate of production incidents from GST rule changes dropped sharply once the model matched the domain.
What this looked like in the code
A few concrete pieces, because architecture posts without code are just vibes.
The old invoice write path looked roughly like:
invoice = Invoice.create(...)
tax = calculate_gst(invoice)
invoice.update(cgst=tax.cgst, sgst=tax.sgst, igst=tax.igst)
The new write path looked like:
event = SupplyEvent.append(
kind='invoice_issued',
supplier_state_id=supplier.current_state_id,
recipient_state_id=recipient.current_state_id,
line_items=lines,
occurred_at=now
)
projection = SupplyProjection.apply(event)
# projection contains the tax entries, the GSTR bucket, the ITC eligibility
The Invoice object still existed for UI and PDF purposes — users need to see and print invoices — but it was now a read model built from events, not the source of truth. The source of truth was the event log.
Reconciliation became a diff between our projection of GSTR-2B (from our event log) and the actual GSTR-2B pulled from GSTN. Mismatches became structured — missing event, extra event, party-state mismatch — instead of "numbers don't match, good luck."
The migration was the hard part
Redesigning was one week of whiteboarding. Migrating a live product with existing users, existing filings, and existing legal exposure was the actual work.
A few decisions that mattered:
- Dual-write before dual-read. For a period, every invoice write went to both the old table and the new event log. We compared the projections to the legacy calculations nightly. Discrepancies were the bug list.
- Never backfill silently. Historical invoices were replayed into the event log with a
source=legacy_importflag. Filings generated from imported data were marked as such in the UI so users and their CAs could audit. - Freeze filed periods. Once a GSTR-1 was filed, that period's event log became append-only with amendment events, not edit events. This matches how GST actually works — you file amendments, you don't edit history — and it removed an entire class of "why did my last quarter change?" support tickets.
- Kept the old code path behind a feature flag for six months. Flip per tenant. Roll back per tenant. Never a big-bang cutover on a compliance product.
What moved
We are not going to publish exact incident-rate numbers here because the interesting metrics are qualitative and the product team owns the quantitative disclosures. The directional outcomes:
- The class of "silent wrong output" bugs — the ones where a filing went through but was incorrect — largely disappeared, because incorrect states now surface as impossible transitions at write time, not as wrong numbers at read time.
- New GST notifications became a config change plus tests, not a release. When the government introduced changes to e-invoicing thresholds, the taxpayer state model absorbed it without a data migration.
- Support load per active user dropped meaningfully once the reconciler stopped being the front line of correctness.
- The GSTR-2B reconciliation feature — which had been unshippable under the old model because we could not reliably diff two heterogeneous representations — shipped and became a retention driver.
GimBooks went on to serve millions of Indian SMBs with this backbone. The product surface expanded to e-way bills, e-invoicing, and inventory. None of it required rearchitecting the tax layer again, which is the actual test of whether the model is right.
What generalizes to your compliance SaaS
If you are a fintech founder reading this because your GST feature works most of the time and you are scared of the edge cases, here is what transfers directly.
1. Model the domain, not the output
If your tax code is a function that takes an invoice and returns tax numbers, you are modeling the output. The domain is the sequence of legal events that produced the invoice. Model that. The tax numbers fall out as a projection.
2. Party state is versioned or you are wrong
Every party in your system — the taxpayer, every counterparty — has a registration status that changes over time. If your database has a single gstin_type column with the current value, every historical filing you generate is a lie waiting to be discovered. Version it. Timestamp every change. Query as of a date, not as of now.
3. Events, not edits
Filed periods are immutable in the eyes of the tax authority. Your data model should enforce this before your UI does. Amendments are new events referencing old events. This one decision eliminates an entire category of correctness bugs.
4. Reconciliation is a first-class product surface
GSTR-2B vs your books is the single most valuable feature for a serious accounting user. It is impossible to build well on top of a mutable, calculated tax model. It is straightforward on top of an event log. If you plan to compete on reconciliation, the data model decision is upstream of the feature decision.
5. Feature-flag every compliance change per tenant
Compliance products cannot big-bang. Roll changes tenant by tenant, compare outputs against the old path, and keep the fallback alive longer than feels comfortable.
What this approach is bad at
Honest tradeoffs, because we said we would.
- Higher write complexity. Every invoice write now touches an event log, a projection, and party-state validation. For a tiny team early in product-market fit, this is overkill. Do not build this at 500 users.
- Storage grows faster. Event logs are append-only. You will spend more on storage than you would with a mutable-row model. Cheap, but non-zero.
- Debugging is different. "Why is this number wrong?" becomes "replay the event stream and inspect projections at each step." Powerful, but engineers unfamiliar with event-sourced systems will complain for the first two months.
- Reporting queries need read models. You cannot ad-hoc SQL against the event log for a dashboard. You need to precompute projections for every reporting surface. This is more upfront work.
The pattern is right when correctness under regulatory change matters more than write throughput or reporting flexibility. For GST compliance SaaS, that is always. For a general-purpose invoicing tool with no tax logic, probably not.
How CodeNicely can help
We built the engineering backbone of GimBooks — the YC-backed Indian accounting SaaS this post is about — through the transitions described above. If you are running a compliance or accounting product that works at 50K users but you can see the edge-case failures accumulating, the engagement that matches your situation is not a rewrite. It is a targeted architectural intervention on the domain model, with dual-write migration so the live product keeps running.
The specific things we bring: engineers who have shipped event-sourced compliance systems in production against live GSTN endpoints, familiarity with the actual pattern of CBIC notifications and how they mutate a tax model over time, and the discipline to feature-flag per tenant rather than cut over. Full IP ownership, no vendor lock-in. If you want to talk through your specific edge cases, reach out and we will do the diagnostic before we propose the work.
Other case studies with adjacent patterns: Cashpo (lending, KYC state machines, credit scoring pipelines) and Vahak (logistics marketplace with multi-party state).
Frequently Asked Questions
Do I need to rewrite my accounting SaaS if GST edge cases are breaking?
Usually no. In most cases we have seen, the fix is a targeted change to the party-state and event models with dual-write migration, not a full rewrite. The application layer, UI, and reporting can stay. What changes is the source of truth for tax-relevant events. A rewrite is only warranted if the existing data model has no place for versioned party state at all — and even then, incremental migration beats big-bang.
How do you handle GST notifications that change tax rules mid-quarter?
In an event-sourced model, each event carries a reference to the party states and rule versions in effect when it occurred. When rules change, you add a new rule version with an effective date. Events after that date use the new rules; events before it project against the old ones. Amendments to old periods use the old rule set automatically. This is the specific pattern that removed the largest class of production incidents at GimBooks.
What is the difference between a rules engine and a state machine for GST logic?
A rules engine answers "given these facts, what is the decision?" It is stateless. GST is stateful — the correct treatment of any transaction depends on the history of the parties and the supply chain. A state machine over an event log encodes that history explicitly. Rules engines are fine as a component (for the actual rate lookup, say) but they cannot be the top-level abstraction for a compliance product.
How long does an architectural migration like this take?
It depends heavily on your current data model, tenant count, and how much historical data has to be replayed for existing filings to remain consistent. For a personalized assessment, contact CodeNicely with your current schema and traffic profile and we will scope it honestly.
Can we do this without downtime for existing users?
Yes, if you commit to dual-write and per-tenant feature flags. The pattern is: write to both old and new models in parallel, reconcile nightly, migrate tenants one at a time once their projections match, and keep the old path available as a fallback for months, not weeks. Every compliance migration we have shipped has followed this pattern precisely because filed data cannot be corrupted mid-flight.
Building something in Fintech?
CodeNicely partners with founders and tech teams to ship AI-native products that move metrics. Tell us about the problem you're solving.
Talk to our team_1751731246795-BygAaJJK.png)