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:
- Get one design partner who genuinely needs it and will use it daily. Build with them, not for a hypothetical market.
- Vertical first. “ERP for garment manufacturers in Bangladesh” sells. “ERP” does not.
- Migration is your biggest sales obstacle. Build import tooling early — customers ask “how do I get my data in?” before they ask about features.
- Accounting credibility is the trust barrier. Get a qualified accountant to review your ledger design before you sell to anyone.
- 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.
- Support is the business. ERP customers need help at month-end, in their timezone. Price for it.
- 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.