跳到正文
DEV Community· Jeff Tsai·· 4 小時前AI 評分33

Day 1:為何 ERP 需要以定義為核心的框架

Day 1: Why ERP Needs a Definition-Driven Framework

AI 導讀

這篇系列首篇文章探討 ERP 為何需要以定義為核心的框架,並以 Northwind 範例資料庫上的應用作為驗證案例。文章指出,在典型的 Code-First 技術堆疊中,新增一個欄位會在 13 個位置重述同一事實,即使部分內容可以自動產生,副本不一致仍可能造成問題。作者亦介紹以 Claude Code 建構的開源框架 bee-library,並説明系列將討論框架機制及其設計取捨。

正文

This is the first post in a thirty-part series, ERP Architect's Notes: Designing a Definition-Driven Framework.

What follows is not about building one form with some framework. It is about designing a framework that can drive hundreds of forms: which decisions it should make on your behalf, which ones it should leave to you, and why each mechanism ended up shaped the way it is. Every claim is checked against a working application built on the Northwind sample database, but the case study is evidence, not the subject.

This post covers:

  1. The three layers of ERP development cost: a single change, scale, and delivering customization
  2. The three long-standing problems those layers converge on, and why tooling does not remove them
  3. The point where the paradigms diverge: is the single source of truth code, or definitions?
  4. A controlled experiment: how many places does one field touch under each paradigm?

Who I am

I have spent more than twenty years as a software architect in packaged ERP and HRM products. Packaged software has a very long product cycle, ten or twenty years is normal, and over that span the system is not merely maintained. It has to grow a new face every time the front-end generation turns over. I have been through three of them: desktop, web, and mobile apps. Each time the question was the same. Do we rebuild all several hundred screens?

On a product line like that, an architectural decision does not get rewritten by someone else three years later. You watch it grow for ten years and then keep maintaining it. I stayed in the same role the whole time. I never moved into managing projects or teams and left the code behind, so I did not miss the consequences of a single one of those decisions.

About the framework, and about this series

Some of the ideas I accumulated over those years were built inside real projects. More of them were only ever thought through, because writing all of it alone was never worth the cost. That cost is different now. I used Claude Code to build the ones I never got to, and the result is an open source framework, bee-library. The Northwind case study in later posts runs on it.

The case study itself lives in a separate repository, bee-northwind-avalonia. The framework repository holds the mechanisms; the case study repository holds only the application's own definition files and code, with the framework pulled in as a package. If you want to know what applying this framework actually requires, which definition files you prepare and what code you still have to write, the case study repository shows it more clearly than the framework does.

The framework is still moving. Things that deserve to become mechanisms will keep being folded in, interfaces will change with them, and breaking changes will happen. If you want to reference the packages directly today, that has to be part of your calculation. You can also clone the whole thing and shape it into what you need.

This series is not a manual for it. The framework has exactly one job here: to show that these ideas are not just talk. What the thirty posts discuss is the mechanisms that already exist, what problem each one ran into, how it was solved, what it cost, and why that option rather than another. Implementation details are not walked through line by line. You are entirely free not to use it, and to take only the idea that definitions should be the source, and build it in whatever language and tools you already know.


The whole series rests on one sentence:

The complexity of an ERP is not in how hard any single feature is. It is in how many times the same fact gets restated, multiplied by how many tables, multiplied again by how many customizations.


1. The cost of a single change: how many places one field lands in

The requirement: orders need one more field, Required Date. No calculation, no dependent behavior, no effect on existing logic. The user says this is "just one field, it should be easy," and the user is right.

In a typical Code-First stack, ORM plus web API plus front end, that field lands in thirteen places:

# Location What changes
1 Entity class Add a property
2 Type mapping configuration Precision, nullability, column name mapping
3 Migration Generate it, review it, commit it, run it at deployment
4 Query projections The Select for list queries, the column list for reports
5 DTOs Once for the request, once for the response
6 Mapper configuration Entity to DTO
7 Validation Required, range, ordering relative to the order date
8 API contract The OpenAPI schema, republished to consumers
9 Front-end types A TypeScript interface or a C# model
10 Front-end form Control, label, position in the layout
11 Front-end list Column, width, sorting
12 Localization resources One per language
13 Test data Fixtures, seeds, fake data generators

To be fair, several of those can be generated. Migrations come from a tool, OpenAPI is produced by scanning assemblies, front-end types can be generated from OpenAPI. The number you actually hand write is closer to five or six.

But the number you hand write is not the point:

The single fact that "Required Date is a date, it is required, and it shows after the order date" has been restated thirteen times in this stack.

Every restatement is a place where the copies can disagree:

  • Forgot the DTO, and the value reaches the database but never the front end
  • Forgot the validation, and users submit it empty
  • Forgot the list column, and users ask why they cannot see it on the query screen

None of these are hard problems. They are counting problems, and each inconsistency has to be found by a test or by a user report.

A team boundary runs straight through that table

Items 1 through 8 are back end. Items 9 through 13 are front end. If those are two teams, the line cuts through the middle of the table, and every item that crosses it changes character. It stops being editing and becomes coordination.

A request to add one field then grows into this:

  • A meeting to agree the contract first, then separate scheduling on each side
  • Sequencing: until the back end ships there is nothing for the front end to consume, so starting early means hand rolling a mock
  • Release windows: whichever side goes first has to cope with the other not having caught up
  • And during integration, the discovery that one side read "required" as a front end block and the other read it as a back end block

Not one of those activities is writing the field. What they consume is not effort, it is waiting. The user counts the time from asking to shipping; the engineer counts the time actually spent working. So one side asks why changing a single field takes this long, and the other side does not feel it was slacking. Both are right, because the cost is real, it just does not land on the part where anyone is typing.


2. The cost of scale: multiply that table by a thousand

A full packaged ERP covers finance, purchasing, inventory, manufacturing, sales and HR. Business tables run into the thousands.

The founding premise of an ORM is that objects map to tables, so:

thousands of business tables
  -> thousands of entity classes
  -> plus DTOs, usually more than one: List / Detail / Create / Update
  -> plus mapper configuration
  -> plus repositories

The overwhelming majority of those classes resemble each other. The entity for Supplier and the entity for Shipper differ in field names and field count; structurally they came from the same mold.

This is not an argument that ORMs are bad. An ORM works well in the setting it was designed for. The problem is quantity. When the same pattern has to be copied a few thousand times, the copying itself becomes the dominant cost.

Relationships then multiply that quantity again:

  • ERP tables are densely connected by nature. An order points at a customer, a salesperson and a shipper; a shipment points at an order, a warehouse and a lot number.
  • Every one of those relationships needs someone to decide how it is expressed, how it is loaded, and who maintains it. Each decision is reasonable on its own. Spread a few thousand tables out at once and nobody can hold the whole picture.

Take the thirteen slots from the first section and multiply them by a few thousand, and multiply the team boundary along with them. Cross team coordination is not paid once per table, it is paid once per request, and the number of requests is itself proportional to the number of tables.

This is not inconvenience. It is loss of control. Anyone who has maintained a large system recognizes the symptoms:

  • Nobody dares touch shared classes, because nobody is sure who depends on them
  • New joiners take a long time before they will go near core modules
  • The same concept has several different spellings across modules, because different people grew them at different times

3. The cost of customization: how do you deliver it once it is done

The first two layers are about how much work a change takes. The third one is more fundamental, and there is no way around it, because:

An ERP that gets installed and simply used essentially does not exist.

Every rollout comes with customization. This client numbers documents differently, that one calculates cost differently, this department wants two extra fields, that report needs a different grouping. The only variable is the path the customization travels down.

Under Code-First, all thirteen of those places are code, and code has exactly one delivery path:

edit -> compile -> test -> package -> deploy

The same path hurts in three different ways depending on the deployment shape:

Deployment shape Where customization hurts
A single organization's internal system The simplest case: schedule one maintenance window. But changing a decimal scale from 2 to 4 still means a full build, test and deploy
Packaged software sold to many customers, each with their own deployment Only two options. Put it in the trunk behind a switch, and the switches accumulate until nobody dares delete any of them. Or branch, and every branch evolves on its own while every fix in the trunk has to be carried across one at a time
Multi-tenant SaaS on shared assemblies Tenant A changes one formula, and that means coordinating a maintenance window across all tenants and regression testing every tenant's functionality. If any one tenant breaks, the whole batch rolls back

Three shapes, one conclusion:

The size of the requirement and the cost of delivering it have come completely apart.

Whether the requirement is changing a decimal scale from 2 to 4 or reworking an entire module, it travels the same delivery path and carries the same deployment risk. The result is that small requirements have to be scheduled into the release rhythm too, and the engineering team is forced to treat every change as a significant event.

The root of that decoupling is that customization was expressed as code. The deployment shape only decides how much it hurts. It does not decide whether it hurts.

This layer is what definition-driven design is actually for:

Definitions are data, not code. Changing data requires neither recompilation nor redeployment of assemblies.


4. Three problems that never got solved

The costs from the first three sections converge into these:

Long-standing problem Symptom
Specifications scattered across three layers The same field is defined once in the UI, once in the DTO and once in a database migration, and they drift apart easily
Business logic scattered Different modules are maintained by different engineers, styles diverge, and the same work gets done more than once
Customization that resists standardization Custom logic cannot flow back into the standard product, and accumulates as technical debt nobody can govern

For the past twenty years the usual answer has been more tooling: code generators, project templates, smarter mappers, more automatic migrations. Those tools genuinely made each restatement cheaper.

What they did not change is the structure in which one fact gets restated thirteen times. They only made restating it faster.

For the third layer, customization cost, tooling helps even less. Generated code is still code, and still has to be compiled and deployed. The reason the third problem has been open for twenty years is right here.


5. Where the paradigms diverge

The problem is not the tooling. It is the paradigm. There is only one question to answer:

Is the single source of truth in this system the code, or the definitions?

  • Code-First: the entity class is the source. The database is derived from it through migrations, the API contract is derived from it through OpenAPI, and front-end types are derived from that. Definition files are a by-product.
  • Definition-Driven: a structural description is the source. The UI, the database, the CRUD SQL and the validation rules are all derived from it. Code appears only where a real business judgment has to be made.

The difference is not that a file extension changed from .cs to .xml. It is the nature of the source:

Code is the source Definitions are the source
How a thousand tables are represented A thousand classes A thousand definition records
What you can bring to bear on them Compilers, IDEs, refactoring tools All of that, plus batch scripts, generators, programmatic checks, and AI
The delivery path for a change Compile, package, deploy Swap in a different definition
How customization is represented Branches or switches scattered through source A layer of data stacked on top of the standard definition
How the front end learns which fields exist A second, hand-written set of types kept in sync by contract and by people Fetching the same definition at run time
What a new front-end generation costs Rebuilding every screen Writing one new rendering layer
Who can change it Developers Developers, consultants, tools, and even users

Two of those rows deserve more than a line:

  • A thousand classes and a thousand definition records are the same count, but managing them is not the same order of magnitude. A thousand structurally identical XML files can be swept by one script that checks whether every amount field has the right decimal scale. That takes minutes. Doing the equivalent check across a thousand classes is considerably harder.
  • Generational cost is the one I feel most strongly. Front-end technology turns over roughly every ten years. If screens are written by hand one at a time, a generation change means rebuilding all of them. If screens grow out of definitions, a generation change means writing one new rendering layer. The more forms you have, the wider the gap.

That is also the reason layout deserves to be its own layer of definition. Structure is stable and follows the business; layout is volatile and follows the front-end generation. Once they are separated, what gets rewritten at generation change is the rendering layer, not the thousands of structural definitions.


6. A controlled experiment: how many places for the same field

Back to adding Required Date to orders. I counted it in this series' case study application, and the answer is three definition edits and zero lines of code.

Table structure:

<DbField FieldName="required_date" Caption="Required Date" DbType="Date" />

Form structure:

<FormField FieldName="required_date" Caption="Required Date" DbType="Date" />

Layout:

<LayoutField FieldName="required_date" Caption="Required Date" />

What follows automatically after that: the database column, the form control, the list column, the SQL for insert, update and delete, and transport across layers.

  • No entity class to keep in sync, because the SQL is assembled from the definition at run time
  • No DTO or mapper to keep in sync, because the data crossing layers carries its own structural description

The count moves by one in either direction depending on the situation, but the count is not the point. The point is that all three places are definition data.

What happened to the team boundary

The front end no longer declares the field a second time. It asks the back end for that definition at run time and renders accordingly:

front end starts -> asks the back end for the definition -> grows controls, list columns and validation from it
  • The contract does not have to be negotiated, because the contract is that definition. There is no second copy that could disagree with it.
  • There is nothing to wait for. The back end changes the definition, and the front end gets the new one the next time it asks.
  • The coordination, scheduling, sequencing and release windows from the first section mostly just disappear.

What is left is half of the first step: what this field is called in business terms, where it goes, and whether it is required. Somebody was always going to decide that. It is a business judgment, not communication overhead.

The boundary has not vanished, though. It moved.

  • The front end still needs a rendering layer that knows how to read definitions, and somebody has to build it. The difference is that it gets built once, and after that every form and every field is free.
  • The team boundary drops from "every field" down to "the rendering layer," and the frequency of conversation changes from "every request" to "whenever framework capability changes."
  • Anything that genuinely cannot be declared as a definition still has to be discussed. A rule about when a document may not be shipped still needs someone to decide who blocks it, at which layer, and what the message says. No architecture escapes that. What definition-driven design can eliminate is structural restatement, not semantic negotiation.

After the change lands

Two practical problems remain, and the framework has to absorb both:

  • The physical database does not have that column yet. The framework compares the definition against the actual structure and adds the column, so nobody hand writes a migration. A later post takes this apart.
  • The system is running, so how does it know the definition changed? The node that edited the definition has to be able to notify the others so their cached copies are invalidated. Cross-Node Invalidation for Definition and Data Caches covers that.

The point is one sentence: between changing a definition and having it take effect there is no compilation, no packaging and no assembly deployment.


7. A map of the thirty posts

Chapter Theme
One (posts 1 to 3) Why Definition-Driven
Two (posts 4 to 8) Definition Layer
Three (posts 9 to 12) Data Access and Caching
Four (posts 13 to 18) Business Logic and API
Five (posts 19 to 21) Multi-Tenancy and Customization
Six (posts 22 to 25) Transport, Security and Audit
Seven (posts 26 to 28) Data Semantics
Eight (posts 29 to 30) Case Review and The Big Picture

Every post is written roughly the same way. First what the problem requires you to weigh, then how the framework is designed and why it was designed that way, and where a case study can verify it, a closing section with the actual Northwind definitions.

Two things to be clear about up front:

  • The subject is long term cost, not development speed. In the short term Code-First wins almost every time: mature tooling, a complete ecosystem, a fast start. The cost of definition-driven design is concentrated at the beginning, and the return does not show until year three or year five. If your system ships, closes, and gets rewritten wholesale three years later, this series is of limited value to you. It is written for people who know they will be maintaining something for a long time.
  • Trade-offs and costs will be written as plainly as benefits. The value of a framework is not in what it claims to do. It is in knowing precisely what it does not do, and why. Highly dynamic UIs, dashboards, reports and anything that is not form-shaped will only hurt more if you force them into this model.

A thread to pick up later

The next post, An Overview of Definition-Driven Architecture, is about which decisions a framework should make on its users' behalf. The one after that, The Northwind Case Study: Eight Forms and a Custom Business Object, brings the case study on stage.

The case study builds a complete application out of Northwind: eight forms, master-detail, three cross-table relationships, validation, all of it. Exactly one of those forms needs code written for it. What is inside that one, and which places we deliberately chose not to express as definitions, gets reconciled item by item in Which Part Definitions, the Framework and Application Code Each Own.

The enemy of ERP is not complexity. It is restatement. And the reason tooling cannot eliminate restatement is that restatement is a product of the paradigm. As long as code is the source, the same fact is condemned to be said many times over.


📚 Part of the thirty-post series ERP Architect's Notes: Designing a Definition-Driven Framework

📘 Original article on HackMD, in Traditional Chinese:
👉 https://hackmd.io/@jeff377/day-01-why-definition-driven

📢 You are welcome to republish this, please credit the source.
📬 Follow for more engineering notes:
Facebook | HackMD | GitHub | NuGet

來源:DEV Community · dev.to