Building Your Own ERP

Sometimes it’s the right call: a vertical nobody serves, a market where existing products are unaffordable, or a product opportunity. More often it isn’t. This page is honest about both, and gives you a concrete plan if you proceed.

When it’s justified

✅ A vertical no product serves properly — Bangladeshi RMG bonded-warehouse compliance, a specific agricultural supply chain, a regulated niche ✅ The market can’t afford existing products — SMBs in emerging markets where SAP B1 is 10× the annual IT budget ✅ You’re building it as a product, not for one company ✅ The scope is genuinely narrow — inventory + invoicing for one industry, not “everything SAP does”

When it isn’t

❌ “Our processes are unique” — they’re 90% standard, and the 10% is usually a workaround for an old system ❌ “It’ll be cheaper” — it will not be. Ever. ❌ “We only need basic accounting” — basic accounting is multi-currency, tax, period close, audit trail, and reconciliation ❌ You have fewer than 3 developers and no domain expert ❌ You need it live in six months

The honest comparison:

Odoo Community + a partner:      $50k–200k implementation, live in 3–6 months
ERPNext + a partner:             $30k–150k, live in 3–6 months
Build your own:                  $500k–3M+, 18–36 months, and you own it forever

Build only if the product opportunity, not the cost, is the reason.

Scope discipline — the make-or-break decision

The failure mode is trying to build everything. Build the spine first:

PHASE 1 — The spine (6–9 months)
  Party master (customers/vendors as one model)
  Item master with UoM
  Chart of accounts + general ledger (double entry, immutable)
  Sales order → delivery → invoice → payment
  Purchase order → receipt → bill → payment
  Basic inventory (quantity + value)
  Users, roles, audit trail
  ← This is a viable product for a small trading business

PHASE 2 — Depth (6–12 months)
  Pricing engine with conditions
  Tax engine + localisation
  Period close, accruals, reconciliation reports
  Reporting framework
  Document generation (PDF, email)
  Multi-currency, multi-company

PHASE 3 — Vertical (ongoing)
  Whatever your niche actually needs — manufacturing, projects, service,
  batch traceability, or the specific compliance your market demands

Do not start with manufacturing. BOM explosion, routing, capacity, and costing are the deepest part of any ERP, and building them before the ledger works is how projects die.

Stack recommendation

Database    PostgreSQL          partitioning, JSONB, RLS, strong constraints
Backend     TypeScript/NestJS   or Python/FastAPI, Java/Spring, C#/.NET, Go
API         REST + OpenAPI      GraphQL for read composition if you want it
Frontend    React or Angular    with a serious data grid component
Queue       Postgres table, or Redis/RabbitMQ when you outgrow it
Cache       Redis
Search      Postgres pg_trgm/tsvector → OpenSearch when needed
Docs        HTML + headless Chrome → PDF
Auth        OIDC (Keycloak, Auth0, Entra ID)
Deploy      Docker + Kubernetes or a managed platform
Observability  OpenTelemetry, structured logs with tenant/correlation IDs

Pick boring, proven technology. You are already taking enormous domain risk; do not add technology risk. PostgreSQL and a mainstream language will not be your problem.

The non-negotiable design decisions

These are the ones you cannot change later.

1. Multi-tenancy

tenant_id as the first column of every table and every index, from the first migration, even if you launch with one customer. See Multi-tenancy.

2. The ledger is append-only

CREATE TABLE journal_line (
  id BIGSERIAL PRIMARY KEY,
  tenant_id BIGINT NOT NULL,
  header_id BIGINT NOT NULL,
  account_id BIGINT NOT NULL,
  amount NUMERIC(19,4) NOT NULL,      -- signed: + debit, − credit
  ...
);
REVOKE UPDATE, DELETE ON journal_line FROM app_user;

Enforce it at the database level, not by convention.

3. Money is NUMERIC, always with a currency

amount        NUMERIC(19,4) NOT NULL,
currency_code CHAR(3) NOT NULL,
amount_local  NUMERIC(19,4) NOT NULL,
exchange_rate NUMERIC(19,9) NOT NULL

Never a float. Never an amount column without its currency.

4. Effective dating on master data

valid_from DATE NOT NULL,
valid_to   DATE NOT NULL DEFAULT '9999-12-31'

5. Document flow from day one

CREATE TABLE document_flow (
  tenant_id, predecessor_type, predecessor_id, predecessor_line,
  successor_type, successor_id, successor_line,
  quantity_referenced NUMERIC(18,6), value_referenced NUMERIC(19,4)
);

Retrofitting this is close to impossible.

6. Audit trail on everything

CREATE TABLE audit_log (
  id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL,
  entity_type TEXT, entity_id BIGINT, action TEXT,
  field_name TEXT, old_value TEXT, new_value TEXT,
  user_id BIGINT, occurred_at TIMESTAMPTZ, correlation_id TEXT
);

7. A single posting service

Every journal line in the entire system is written by one service, which enforces: period open, accounts valid, dimensions valid, currency triple populated, and sum = 0. No module writes journal lines directly. No exceptions, ever — the first exception is the beginning of the end.

8. Idempotency on every write API

See Concurrency and Idempotency.

9. Number ranges as a real object

Not a database sequence. See Number Ranges.

10. Configuration as data

Document types, posting rules, approval thresholds, tax codes, account determination — all in tables the customer can edit, not in code.

Reference schema sketch

-- Organisation
tenant, company, site, storage_location, cost_center, profit_center

-- Master data
party, party_role, party_address, party_bank_account, party_org_data
item, item_uom_conversion, item_org_data, item_valuation
gl_account, chart_of_accounts, tax_code, payment_term, currency, exchange_rate

-- Configuration
document_type, number_range, account_determination, approval_matrix,
posting_rule, validation_rule, price_condition_type, access_sequence

-- Transactions
sales_order / _line / _schedule_line
delivery / _line
sales_invoice / _line
purchase_order / _line
goods_receipt / _line
purchase_invoice / _line
payment / _allocation
material_document / _line
journal_header / journal_line
document_flow

-- Derived (rebuildable projections)
account_balance_period
stock_balance
open_item

-- Cross-cutting
audit_log, outbox, idempotency_key, job_run, workflow_instance, attachment

Roughly 60–80 tables for a credible Phase 1. Not 5,000 — that’s what twenty years of feature accretion looks like, not what you need on day one.

What people underestimate

Area Why it’s harder than it looks
Tax Every country differs; rates change; e-invoicing mandates. Buy this
Multi-currency Rate types, revaluation, translation, rounding, three currency columns
Period close Accruals, reversals, reconciliation, locks, ordered dependencies
Pricing Conditions, scales, validity, hierarchies — it’s a rules engine
Document numbering Gapless legal requirements vs concurrency
Reporting Users want 200 reports and every one must reconcile
Data migration The customer’s legacy data is worse than they told you
Concurrency Bugs appear at month-end, in production, at scale
Printing Fonts, page breaks, multi-language, 2,000-line invoices
Permissions Field-level, org-level, row-level, all at once

Tax and payroll: buy, don’t build. Both are moving legal targets that will consume a developer permanently.

Testing

Unit tests            calculation logic — pricing, tax, costing, depreciation
Integration tests     full process flows: order → delivery → invoice → payment
Posting tests         assert the exact GL entries for each business event
Reconciliation tests  subledger totals = GL, always, after any sequence of operations
Property tests        random sequences of operations, then assert every invariant
Concurrency tests     parallel clients on the same records
Volume tests          production-scale data before go-live
Migration tests       every schema change against a full-size copy

The property test is the highest-value one:

For any random sequence of postings:
  - every document balances to zero
  - trial balance totals zero
  - AR subledger = AR control account
  - AP subledger = AP control account
  - inventory quantity × valuation = inventory GL account
  - no journal line was ever updated or deleted

Run it with thousands of generated sequences. It finds the bugs that matter.

Go-to-market realities

If you’re building a product:

  1. Get one design partner who genuinely needs it and will use it daily. Build with them, not for a hypothetical market.
  2. Vertical first. “ERP for garment manufacturers in Bangladesh” sells. “ERP” does not.
  3. Migration is your biggest sales obstacle. Build import tooling early — customers ask “how do I get my data in?” before they ask about features.
  4. Accounting credibility is the trust barrier. Get a qualified accountant to review your ledger design before you sell to anyone.
  5. Compliance is the moat. Local tax, local e-invoicing, local statutory reports. Nobody switches ERP for a nicer UI; they switch when compliance forces them.
  6. Support is the business. ERP customers need help at month-end, in their timezone. Price for it.
  7. Expect a 12–18 month sales cycle for anything above small business.

Study these codebases

Before writing a line, read:

  • ERPNext (erpnext/accounts/) — the clearest readable implementation of a GL, stock ledger, and submittable document pattern
  • Odoo (addons/account/, addons/stock/) — a mature, production-grade implementation
  • Frappe framework — the best metadata-driven design to learn from

A week reading erpnext/accounts/general_ledger.py and erpnext/stock/stock_ledger.py will teach you more than a month of design meetings.

The honest summary

Building an ERP is one of the hardest things in application software: broad scope, deep domain, unforgiving correctness requirements, and a customer base that cannot tolerate downtime or wrong numbers.

But the domain knowledge is durable, the problems are genuinely interesting, and a focused vertical product in an underserved market is a real business.

Just don’t start with manufacturing, don’t build your own tax engine, and get the ledger right before anything else.

Next