Engineering Journal · TECHNOLOGY · August 16, 2023 · 17 min read
An ERP Is a Record of How the Business Works
Back-office platforms fail when they are designed as forms on tables. They have to hold history, exceptions, and the argument the organisation is still having with itself.
By Golam Sorwar, Tech Lead and Full Stack Engineer in Dublin.
I have built and inherited the kind of software people casually call ERP: school administration, later college operations, earlier a travel back-office with agents and bookings. From the outside it looks like screens and tables. From the inside it is an argument the organisation is having with its own past.
That is the distinction I wish I had been able to name earlier. A CRUD application stores the current answer. An operational system stores how the answer was allowed to become true, including the times it was not quite true and someone still had to proceed.
If you design the second thing as if it were the first, you will get a tidy schema and a messy week.
Context
These systems sit under people who do not think in resources. They think in terms, invoices, attendance, visas, rooms, flights, agent commissions. The software is asked to be the place those nouns stop being rumours.
The users are staff who will be there after you ship. They will invent exceptions because the official process never covered Tuesday. They will also be asked for reports that assume the official process was followed. That contradiction is the product.
I have also seen marketplace and multi-tenant back-offices with the same shape: many clients, one codebase, a lot of "just this customer." The label ERP is less important than the load. The load is operational memory.
Finance and compliance make this sharper, but they are not required for the pattern. Attendance, accommodation, agent bookings — anything that will be argued about next month — belongs in the same family. If a manager will ask "what was true then," you are in ERP territory even if nobody uses the word.
The problem
The apparent problem is missing features. We need another module. Another form. Another export.
The real problem is that the business is not a set of current rows. It is a set of rules that change, plus a history that must remain explainable after the rules change. A student's status last March still matters in September. A booking that was cancelled still matters to a refund. A price that was wrong and then corrected still matters to an audit.
Ordinary CRUD is allergic to that. It wants the latest name, the latest flag, the latest total. Operations want to know what was true when a decision was taken.
That allergy shows up in the smallest designs. A student name change that overwrites the name on a letter already sent. A price edit that changes a booking already invoiced. A status that means "current" and also means "as of the last save," which are not the same sentence. CRUD invites you to believe the row is the world. In an operational system the row is a view of a story, and the story has already been told to someone else.
The tempting solution
Model the nouns you can see. Users, orders, invoices, students, rooms. Give them create-read-update-delete. Add a status enum when someone complains. Add a notes field when the enum fails. Add an admin who can edit anything when the notes field fails.
This produces software that looks complete in a demo. You can click through a happy life. The unhappy life — the one that pays the salaries of the people in the office — lives in the notes field.
Why that is not enough
Business rules in these systems are rarely predicates on a single row. They are sequences. You may not invoice until X. You may not change Y after Z has been sent. You may change Y if a named role records a reason. That is not a form validation. That is a process.
Permissions follow the process, not the table. The same person may create a record and not be allowed to reverse the financial consequence of it. If your authorisation model is "can update invoices," you have already lost the plot.
Reporting wants shapes the transactional model does not enjoy. Month-end, attendance windows, agent statements. If you only have the live row, you will rewrite history every time you "fix" a field.
Integrations then attach to whatever you called the current truth. Accounting, email, a mobile app. They amplify whatever sloppiness you hid in CRUD.
There is also a staffing consequence. CRUD systems train developers to think in resources. Then a request arrives that is actually a period close, or a status that must not move after a document was issued, and the team reaches for another column. The column works for a week. The report breaks in a month. The organisation learns that engineering "does not understand the business." What they mean is that we modelled a form.
I have more sympathy for that accusation than I used to. If your schema cannot say what was true in a previous window, you will keep asking operations to be the memory. They will, and they will stop trusting the screens.
Options
Stay CRUD and let staff keep a spreadsheet for the real process. Advantage: the application stays simple. Disadvantage: the spreadsheet becomes the ERP, and your software becomes a data-entry tax.
Buy a generic ERP and customise it until it resembles the organisation. Advantage: you inherit a vendor's gravity. Disadvantage: you inherit their nouns, and the customisation becomes a second product you do not fully own. Sometimes still correct. Not a way to avoid understanding the domain.
Build a domain model around transitions, documents, and periods, even if the UI still looks like forms. Advantage: the software can explain itself. Disadvantage: slower to the first screen. You will be accused of over-engineering by anyone who has only seen the demo path.
Event-source everything. Advantage: history is native. Disadvantage: you have bought a research project. I want history where money, status, and compliance live. I do not want an event log for every typo in an address.
Trade-offs
You trade the pleasure of a thin resource API for the burden of saying what a change means. That burden is the job.
You also trade some user freedom. A good operational system refuses edits that would make the past unintelligible. Staff will call that rigidity. The alternative is a system that agrees with whoever last saved.
You should not trade away a way to record an exception. Exceptions will happen. If they cannot be recorded, they will be hidden.
Decision
I design these products as records of decisions, not as bags of fields. A status change is an event with an actor. A financial figure that has been used downstream is not a cell you casually overwrite. A report has a period and a definition, not "whatever the table says tonight."
I still use Laravel and MySQL. This is not a stack speech. It is a refusal to let the framework's resource generators decide the domain.
I also refuse to let the admin user be the process. If the only way to handle Tuesday is "get an admin to edit the row," you have not built an ERP. You have built a database with a dress on. The admin path can exist. It should be a recorded exception, not the weekday.
Implementation / Thinking process
I look for the facts that other departments already treat as evidence: money, attendance, enrolment, fulfilment, anything that has been emailed or invoiced. Those get transitions and reasons. The rest can often stay as ordinary records until they graduate into evidence.
Permissions are named after work, not tables. May issue this letter. May reverse this allocation. May open this period. Table-level CRUD maps poorly onto that and trains you to hand out "update" like a master key.
When a requirement is "we need to change this after the fact," I ask who will read the old value next month. If the answer is finance, auditors, or a student who received a document, we are talking about a new event, not an edit.
I keep a place for the unofficial. A recorded exception with an owner is uglier than a clean model and more honest than a notes column that means everything.
Periods matter. A lot of operational truth is about a term, a month, a booking window. If you only store current flags, you will reconstruct periods from timestamps and get them wrong. I would rather a first-class period on the facts that are reported than a clever query that infers the academic year from created_at.
Permissions, again, follow work. The person who can enter an attendance mark may not be the person who can reopen the window. That is not fussiness. That is how you stop a cleanup from becoming a rewrite of history.
Failure modes
The model stays clean and the office invents parallel books. You will only notice when a report disagrees with a person who is believed.
Someone is given global edit because "they need to fix things." Global edit is how history dies.
A migration or a "data cleanup" rewrites statuses without events. The software becomes confident and wrong.
A new module is built as CRUD because the team is tired, and it becomes the next place exceptions hide. ERP rot is usually local, then contagious.
Operational consequences
Staff trust is binary in these systems. They either use the screen as the story or they use it as a form to satisfy someone else. You can tell which you have by whether they still keep a private file.
Once the system is the story, change gets slower and more valuable. That is not failure. That is the product succeeding at being the record. People who only measure feature count will hate this phase. They are measuring the wrong thing.
The other consequence is political. Once the system is the record, changing a status meaning is a change to how the organisation remembers itself. That is slower than a rename in a CRUD app, and it should be. If you want speed here, you are asking to rewrite history without a meeting.
Lessons
If the organisation would be in trouble if the software forgot last term, you are not building CRUD. You are building memory with rules.
Memory without rules is a junk drawer. Rules without memory are a brochure.
What I would do differently today
I would have named "evidence facts" in the first month of a back-office, instead of discovering them when a report and a human disagreed. The list is usually short. It should be sacred.
I would have been harsher about notes fields. A note is allowed to explain. It is not allowed to be the only place a rule exists.
I would also have drawn the document boundary earlier. A letter, an invoice, an attendance register — anything that left the building — is not a row you tidy later. It is a published fact. Treating published facts as editable fields is how you get a clean demo and an unexplainable September.
Closing thought
An ERP, or whatever you call the operational core, is not a database with forms. It is a record of how the business is allowed to move. Design for the movement and the memory. The forms are how people enter the room, not what the room is for.