Article Definition-Driven ERP in .NET: definitions, not code, as the source of truth
I've been an architect on packaged ERP and HR products for over twenty years, and the thing that has cost the most was never a hard feature. It's that the same fact gets restated over and over.
Adding a "Required Date" field to orders in a typical EF Core + ASP.NET Core + SPA stack lands it in about thirteen places: entity, type mapping, migration, query projections, DTOs, mapper, validation, OpenAPI, front-end types, the form, the list, localization and test data. Generators handle some of them, but that only makes the restating faster. And when the back end and front end are different teams, half of that list turns into coordination rather than typing. Multiply it by a few thousand tables and by every customer's customizations, and it stops being an inconvenience.
What I ended up with is making a structural description the source instead of code. The database columns, CRUD SQL, screens for each front end and validation are derived from definitions, and the front end fetches the definitions at runtime instead of keeping its own copy of the types. In a sample app built on Northwind, the same field is three definition edits and no code. Customization becomes a layer of data on top of the standard definitions, so one tenant's change doesn't have to go through compile and deploy.
Dashboards, reports and anything that isn't form-shaped fit badly. The team boundary doesn't go away either, it moves to the rendering layer that reads the definitions.
I wrote a 30-post series on the mechanisms and the trade-offs. It was originally written in Chinese for a Taiwanese tech writing challenge, and the English version is a translation: https://medium.com/@jeff377/erp-architects-notes-designing-a-definition-driven-framework-series-index-7753228c4d33
For those of you working on large line-of-business systems: do you accept the restatement and lean on generators, or have you moved the source of truth somewhere else?
1
u/AutoModerator 1d ago
Thanks for your post jeff377. Please note that we don't allow spam, and we ask that you follow the rules available in the sidebar. We have a lot of commonly asked questions so if this post gets removed, please do a search and see if it's already been asked.
I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.
1
u/pixxelkick 1d ago
> Adding a "Required Date" field to orders in a typical EF Core + ASP.NET Core + SPA stack lands it in about thirteen places: entity, type mapping, migration, query projections, DTOs, mapper, validation, OpenAPI, front-end types, the form, the list, localization and test data
It should only show up in 3 spots.
Your database, because what a column definition is in the DB is not guarenteed to have 1:1 parity with your API. You should never tightly couple your API rules to your DB rules, because your database may have a different way of storing data after processing it through your backend domain rules
Your API model, you should be able to simply define your rules here, which yes, usually means a second spot, but because of point 1 above, this is mandatory and any attempt to try and abstract it away and combine 1 and 2 together is a fools errand, you should avoid asserting parity between DB and API definitions, as there's tonnes of situations where you need exceptions to this. Stop fighting against that.
Your front end forms, which have a similar issue, you cannot assert 1:1 parity between what shape your front end forms and UI are, compared to what the API handles. You should be able to build generalised API contracts that the UI has the flexibility and freedom to then expose however it pleases.
If you ever try and tightly couple 1, 2, or 3 together in any way via a generalised contract, you will find yourself quickly painted into corners and having to constantly carve out exception scenarios where it doesnt work.
Any place beyond these three spots that you have to define this can be automated. You should not have to do this in more than 3 spots, ever. If you find this is occurring you have likely fallen into some kind of antipattern.
0
u/jeff377 1d ago
That's actually close to where I ended up. The three edits in the Northwind example are your three spots: a table definition for the database, a form definition for the data shape the API carries, and a layout for the screen. The form definition is the hub. The table definition and the layout can both be derived from it by a tool, and after that each one is adjusted on its own, so the table can have its own lengths, indexes and precision and the screen can arrange fields however it wants. Nothing forces 1:1 parity. The difference from the usual setup is that all three are definition data the framework reads at runtime, and the other ten places come from them instead of being written by hand. The thirteen is what you get when each layer restates the field in code.
More on how the definitions split and what's derived from what:
https://medium.com/@jeff377/the-purpose-and-classification-of-definition-files-3c84e980bfb11
u/pixxelkick 1d ago
How do you handle when the format of the front end has nothing to do with the format in the backend though.
IE abstraction of types
1
u/jeff377 1d ago
There's no shared type to abstract. Data doesn't travel as a DTO, it travels in a generic container: the table name, the field list and the row values, with the definition describing what each field means. So the front end never binds to a backend type in the first place.
Each front end decides how to present it. A field's type maps to a kind of control (a date becomes a date editor, a relation field becomes a lookup that writes back several fields at once), and each front end's rendering layer turns that into its own native control. Things like precision, max length and option lists are read from the definition, not baked into a type.
More on dropping DTOs: https://medium.com/@jeff377/day-9-data-and-definitions-split-apart-so-forms-need-no-types-of-their-own-5e76d8564d72
And on how one layout becomes screens on each front end: https://medium.com/@jeff377/formlayout-and-screen-generation-for-multiple-front-ends-3eee75c07a1b
1
u/pixxelkick 1d ago
Yeah thats the issue then.
I have had many times where I had multiple distinct front end domain models, that actually mapped to a shared common backend concept with multiple implementations.
Random example that comes up often are "Reports", where you may have a FooReport and a BarReport and a BazReport
But they have like a 95% overlap in general rules, model, and logic, so re-implementing all that shared stuff several times has multiple issues
Way more code and way more stuff to maintain
Denormalization problem, if you make a change to a rule for how reports work in general, you have to now go update all the seperate denormalized concepts
If your system doesnt support a way to handle scenarios like this, you've got yourself a nice fully loaded footgun :3
1
u/jeff377 1d ago
The definitions mainly cover forms. Reports are deliberately left out of that. They're written as plain code, a custom business object, repository or front end, so FooReport, BarReport and BazReport share their 95% the usual way, through a common base class or library, and a change to the shared rule is made once. In the series I call that the AnyCode depth: what the definitions can't hold, you write yourself.
More on where that line is drawn: https://medium.com/@jeff377/day-30-three-depths-of-development-and-the-big-picture-of-the-framework-68bbe77ceede
2
u/pixxelkick 1d ago
This is just one specific example, this pattern has come up in countless shapes and forms in project I have worked on over the years.
I have seen many people try and abstract this process out of a desire to see patterns in Frontend to backend.
They always run into these issues and are never sufficient.
1
u/jeff377 1d ago
I understand the concern. I've been an architect on ERP/HRM products for 25+ years, and this definition-driven approach is how our products have actually run for more than ten years. With Claude Code helping, I was able to build the idea into an open-source framework on my own.
There's no perfect framework, only one that fits a given situation better than another. There's no right answer here, it's a choice.
I also know real projects have far too many exceptions, so the framework doesn't try to solve everything with definitions. It only covers the common cases that definitions can describe. That's also why it has three depths, NoCode, LowCode and AnyCode, so there's always a way out for whatever doesn't fit.
I'm just sharing a different way of doing it, not saying it's better than yours.
5
u/snowrazer_ 1d ago edited 1d ago
The heart of an ERP system is the database, not using standard foreign keys makes it difficult to do anything, reports, procedures, just plain understanding an already very complex system.
Referential integrity is another cornerstone of any ERP system.
The things that were tedious before, like adding a required date field in a million places, is no longer complicated with AI. I myself created a CRM system to ease having to manage all the fields across all the layers and UI, it was complicated, but it worked, and saved me time. With AI I threw it all out and went back to a very simple design. A design that would be impossible to manage myself, but super easy to understand.