Most software is allowed to be wrong for an hour. You notice, you fix it, you backfill, nobody outside the team finds out. Then one day somebody in a meeting says a number out loud, somebody else asks where it came from, and the answer is the name of your application. That is the day the rules change, and it always arrives before anyone plans for it.
I build this kind of software for a living. Fyuel, an accounting platform we built for the fuel trade, pulls customers, suppliers, tankers, ledgers, banks and reports into one real time system. The consolidation is the entire value of it, and it is also the obligation, because once you are the single place a business looks, being current and being able to defend a figure is the product.
This post is not about accounting. It is about the handful of engineering decisions that quietly change the moment your app becomes the record, every one of which is cheap on day one and painful to retrofit in year two.
You may already be building financial software
The label on the product does not decide this. The test is simpler: if a wrong value on one of your screens would cause somebody to make a payment, chase a customer, file a return or close a period incorrectly, you are building financial software regardless of what the repo is called.
Plenty of products cross that line without anyone noticing the day it happened. Inventory systems that carry value. Billing engines. Commission calculators. Usage meters that feed invoices. Anything that replaced a spreadsheet somebody used to invoice from. The features shipped, the line got crossed, and the reliability bar moved without a ticket.
The balance column is a rumor
The most expensive shortcut in this kind of system is a balance stored in a field and updated in place. It is efficient, it is what every ORM tutorial nudges you toward, and it is what I reached for too before I worked on something where the number was the product.
Here is the problem. The moment two things happen at once, or a job runs twice, or someone corrects an entry from six weeks ago, you have a figure nobody can explain and no way to find out when it went wrong, because the previous value is gone. The column tells you what it currently believes. It cannot tell you why, and it cannot tell you when it stopped being right.
The alternative is old and unglamorous: record the events, derive the totals. A payment received. An invoice issued. A load delivered. An adjustment applied. Each one is a fact with a timestamp and an author, written once and never modified. The balance is calculated from those facts, either on demand or into a cached figure you can throw away and rebuild at any time. The rebuild is the part that matters. If a total is disputed, you can show the entries that produced it, which is the difference between having an answer and having an opinion.
The rule that follows is the one clients push back on hardest, and it is the one worth holding: you do not edit history, you correct it. A wrong entry gets fixed by a reversing entry and a new correct one, both visible, both attributed. That is not bureaucratic caution. It is the only thing that makes last month's report still say what it said last month. A system where somebody can quietly change a figure from six weeks ago is a system where no report can be trusted twice, including the ones that are right.
Reconciliation is a screen, not a support ticket
The numbers inside your software will disagree with the numbers outside it. Routinely. That is normal, not a bug.
The bank statement lands with a fee nobody told you about. The supplier invoice covers a delivery recorded on a different day. The stock count comes back short. If your design assumes agreement, every one of those becomes a support ticket that ends with an engineer running a query against production.
So build for disagreement. Import the outside record, match the obvious items automatically, queue the rest, and let someone explain a difference and write the correcting entry from that same screen. The person doing this work is almost never an engineer, which is exactly the point. Reconciliation is where the business proves to itself that your system is telling the truth, so it deserves a designed screen rather than a report that ends with somebody exporting to a spreadsheet and finishing the job by hand.
Money, units and dates
Three ordinary looking types cause most of the defects I have seen in this category, and all three are nearly free to get right at the start.
Money is not a number. It is an amount plus a currency, stored in the smallest unit, handled with a type that does not drift. Floating point arithmetic on currency is the classic origin of the penny that never balances, and it never announces itself, it just accumulates. Rounding needs a stated rule and place too, because rounding each line and rounding the total give different answers. Both are defensible, so pick one and apply it everywhere, including when something is credited back.
Quantities carry units, and conversion is a business rule. Anyone building for a physical trade meets this in week one. Fuel is measured, moved and billed in units that convert, and the conversion factor is business data with a history, not a constant somebody typed into a formula. Same for weights, hours, packs, cases. When the unit exists only in the column heading, the mistake is not avoided, it is scheduled.
Dates are at least three dates. When the thing happened, when it was recorded, and which accounting period it belongs to are three separate facts, and collapsing them into one created_at is what makes month end miserable later. Keep them distinct and the two features people always ask for late become almost free: closing a period so nothing can be booked into it afterwards, and reporting on what the books said at a point in time rather than only what they say now.
None of this is exotic. It is just work that never shows up as a feature, which is a large part of why a system of record costs more than a product with the same screen count.
Permissions are domain logic, and the audit trail is a feature
In financial software, permissions are not a settings page bolted on before launch. They are part of the model, and the questions are concrete. Who can create an entry. Who can approve one. Can those be the same person above a threshold. Who can void or reverse. Who can reopen a closed period, and does that leave a mark. Who can see margin and supplier pricing, given that plenty of people need the operational screens without the commercial ones. Deciding that before the build is much easier than discovering during rollout that the office shares one login.
The audit trail is the other half and it deserves to be a named feature with an owner, not a log file. A useful one records who did what, when, to which record, with the value before and after, and the finance lead can read it without an engineer running a query. That last clause is the whole distinction. Logs are for debugging. An audit trail is evidence you can hand to an auditor, a lender or an unhappy customer. It is also impossible to backfill, because a trail switched on in year two says nothing about year one.
Two things travel with it. Export has to be real, in a format an accountant actually opens, because your software sits inside a process that does not end at your login screen. And the data needs a restore you have performed, not a backup setting that is switched on. The application can be rebuilt from the repo. The ledger cannot be rebuilt from anything.
What I ask before writing any of it
Four questions, and the answers tell you most of what the build is really going to be.
- Are totals derived from recorded events, or stored and updated in place? If stored, how does a disputed figure get explained?
- How does a mistake get fixed? Listen for reversals and corrections, not edits.
- What happens when the bank, the supplier or the stock count disagrees with us? There should be a screen, not an apology.
- Can we close a period, and can we report on what the books said back then?
Financial software is not harder because the features are complicated. Most of the screens are boring. It is harder because being wrong carries consequences ordinary software does not, and nearly all of that cost sits in the parts a demo never shows.
I wrote a longer version of this for people commissioning the build rather than writing it, over on nullstud.io.
Top comments (0)