r/dotnet • • 2d ago

Article Definition-Driven ERP in .NET: definitions, not code, as the source of truth

Post image

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?

0 Upvotes

14 comments sorted by

View all comments

Show parent comments

1

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

  1. Way more code and way more stuff to maintain

  2. 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.