Loading
Alireza Shokrani

Digital Architect

AI Solutions Engineer

Founder @ CoreBiz ERP

GenAI & RAG Specialist

Full-Stack Systems Developer

Alireza Shokrani

Digital Architect

AI Solutions Engineer

Founder @ CoreBiz ERP

GenAI & RAG Specialist

Full-Stack Systems Developer

Solution

Decoupling Enterprise Business Logic: Architecting a Cloud-Native ERP Backend with Clean Architecture

September 11, 2026 Enterprise Systems Architecture
Decoupling Enterprise Business Logic: Architecting a Cloud-Native ERP Backend with Clean Architecture

A practical engineering guide for designing mission-critical ERP backends — where accounting, inventory, and cross-departmental transactions must remain correct under high concurrency, and technical debt is not an option.

Introduction — The ERP Paradox

Enterprise Resource Planning systems sit at the center of every large organization’s operational reality. They process financial transactions, manage physical inventory, coordinate supply chains, and enforce compliance. They are, by definition, mission-critical.

Yet most ERP systems are architectural paradoxes:

  • They must evolve — business rules change constantly

  • They must remain stable — downtime costs real money

  • They must scale — transaction volumes grow relentlessly

  • They must be auditable — regulators and auditors demand traceability

The traditional response has been monolithic architectures with tightly coupled modules. This works — until it doesn’t. A change in the billing logic breaks inventory reporting. A database schema update destabilizes the general ledger. A new feature introduces technical debt that compounds for years.

This article is about a different approach:

Architecting ERP backends with strict domain boundaries, Clean Architecture principles, and database designs engineered for high-concurrency transaction processing.

The goal is not theoretical elegance. The goal is operability at scale — a system that can evolve without breaking, and perform under load without compromising correctness.

Part 1 — Why Clean Architecture Is Non-Negotiable for ERP

Clean Architecture, as articulated by Robert C. Martin, establishes a fundamental principle:

Dependencies point inward. The domain knows nothing about the infrastructure.

For ERP systems, this is not academic purity — it is a survival requirement.

The Layered Structure

┌─────────────────────────────────────────────────────────────────┐
│                    PRESENTATION LAYER                           │
│              (API Controllers, GraphQL, UI)                     │
├─────────────────────────────────────────────────────────────────┤
│                    APPLICATION LAYER                            │
│     (Use Cases, CQRS Handlers, DTOs, Validation)                │
├─────────────────────────────────────────────────────────────────┤
│                      DOMAIN LAYER                               │
│    (Entities, Value Objects, Domain Services, Events)           │
│                 — NO EXTERNAL DEPENDENCIES —                    │
├─────────────────────────────────────────────────────────────────┤
│                  INFRASTRUCTURE LAYER                           │
│       (Persistence, External APIs, Messaging, Cache)            │
└─────────────────────────────────────────────────────────────────┘

The dependency direction is strictly inward. The Domain Layer defines interfaces; Infrastructure implements them .

Why This Matters for ERP Specifically

ConcernMonolithic ERPClean Architecture ERP
Business rule changesRipple across entire codebaseIsolated to Domain Layer
Database migrationRequires touching business logicInfrastructure swap only
TestingRequires full stack setupDomain logic testable in isolation
Framework upgradesHigh risk, cross-cuttingContained to outer layers
New team membersMust understand everythingCan work layer-by-layer

The cost of change is inversely proportional to the strength of your boundaries.

The Solution Structure (Concrete)

Based on production-ready Clean Architecture ERP implementations :

src/
├── Presentation/
│   └── ERP.API                    # Web API endpoints
├── CompositionRoot/
│   └── ERP.CompositionRoot        # Dependency wiring
├── Core/
│   ├── ERP.Domain                 # Entities + contracts (NO dependencies)
│   ├── ERP.Application            # Use cases, handlers, DTOs
│   └── ERP.Shared                 # Shared primitives
└── Infrastructure/
    ├── ERP.Infrastructure         # External service integrations
    └── ERP.Persistence            # Database access

Key rule: The API project never directly references infrastructure types. All wiring happens in the Composition Root .

Part 2 — Domain-Driven Design: Bounded Contexts for ERP

ERP systems naturally decompose into bounded contexts — each with its own ubiquitous language, its own model, and its own rules.

The 8 Tier-1 ERP Domains

A well-architected ERP system separates global state into isolated execution environments :

DomainResponsibility
FinanceGeneral Ledger, AP/AR, immutable double-entry ledger
Supply ChainPhysical inventory, SKU tracking, procurement
RevenueSales orders, customer relationship mapping
Human CapitalEmployee records, payroll, roles
Enterprise AssetInfrastructure, warehouse management
LegalCompliance, audit logs (SOC2/SOX)
LearningCertifications, training compliance
Master DataThe “Golden Record” — universal ID translation

The Golden Thread: Cross-Domain Communication Without SQL Foreign Keys

Traditional ERP systems use database-level foreign keys to maintain referential integrity across domains. This creates tight coupling at the data layer — the single most damaging form of technical debt in ERP systems.

The architectural alternative: The Golden Thread .

Traditional Approach:
  SalesOrder.customer_id → FOREIGN KEY → Customer.id
  → Database-enforced coupling
  → Cannot scale domains independently
  → Schema changes propagate across domains

Golden Thread Approach:
  SalesOrder.customer_uuid: uuid.UUID  (application-managed pointer)
  → No database-level FK between domains
  → Each domain owns its tables exclusively
  → Domains scale horizontally and independently

Engineering rationale: Foreign keys create implicit coupling. In a high-concurrency system, a bulk delete on a parent table triggers constraint checks across all referencing tables — a performance disaster at scale .

When a customer with 500,000 orders is deleted, the database must validate every foreign key reference. This is why production ERP systems report bulk deletes taking 120+ minutes with 280GB of temporary files .

Distributed Transactions: The Saga Pattern

Without database-level ACID transactions across domains, how do we maintain consistency?

Temporal Sagas — distributed workflows with compensating actions:

Scenario: Order allocation across Supply Chain and Finance
┌─────────────────────────────────────────────────────────────┐
│                    ORDER SAGA WORKFLOW                      │
├─────────────────────────────────────────────────────────────┤
│  1. ReserveInventoryActivity (SCM Domain)                   │
│     └─ SUCCESS → proceed to step 2                          │
│     └─ FAILURE → compensate, abort                          │
│                                                             │
│  2. LockLedgerActivity (Finance Domain)                     │
│     └─ SUCCESS → proceed to step 3                          │
│     └─ FAILURE → trigger ReverseInventoryActivity           │
│                                                             │
│  3. ConfirmOrderActivity (Revenue Domain)                   │
│     └─ SUCCESS → saga complete                              │
│     └─ FAILURE → compensate all previous steps              │
└─────────────────────────────────────────────────────────────┘

This pattern guarantees eventual consistency without phantom locks or distributed deadlocks .

Part 3 — PostgreSQL for High-Concurrency ERP: The Engineering Details

PostgreSQL is the database of choice for most modern ERP systems . But default configurations are not sufficient for high-concurrency transaction processing.

The Foreign Key Indexing Trap

This is the single most common performance oversight in ERP database design.

The fact: PostgreSQL automatically creates an index on the referenced side of a foreign key (the parent table’s primary key). It does not create an index on the referencing side (the foreign key column itself) .

Tables:
  orders (id PRIMARY KEY, customer_id BIGINT, ...)
  customers (id PRIMARY KEY, ...)

PostgreSQL creates automatically:
  INDEX ON customers(id)  ← already the PK, so no-op

PostgreSQL does NOT create:
  INDEX ON orders(customer_id)  ← YOUR RESPONSIBILITY

Consequence: Queries filtering or joining on orders.customer_id perform full table scans. Cascading deletes and foreign key constraint checks also scan the entire table.

Real-world impact from a production ERP workload :

MetricBefore IndexingAfter Indexing
Bulk delete time120+ minutes~62 minutes
Temporary file generation280 GB/day192.9 GB/day
Read IOPS spikes10-15 times/day5-7 times/day
Index scans on account_payment906,000+ in observation window

The fix: Audit every foreign key column. If it’s used in WHERE clauses, JOINs, or cascading operations, index it .

Indexing Strategy for ERP Workloads

ERP databases have distinct access patterns:

Query PatternIndex Strategy
Time-range reports (GL, transactions)Composite index on (entity_id, transaction_date)
Status-based filtering (pending orders, active inventory)Partial indexes on status columns
Point lookups (by ID, by code)Primary key or unique index
Cross-entity joinsIndexed foreign key columns
Aggregate reportsCovering indexes where feasible

Critical insight: Indexes are not free. Every index adds write overhead. For high-transaction ERP systems, the cost/benefit ratio must be measured, not assumed .

Monitoring and Maintenance

High-transaction databases accumulate bloat — dead tuples from updates and deletes .

Essential maintenance for ERP PostgreSQL:

-- Regular VACUUM and ANALYZE
VACUUM ANALYZE orders;
VACUUM ANALYZE gl_entries;

-- Monitor bloat
SELECT schemaname, tablename, 
       pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size,
       n_dead_tup
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;

-- Identify unused indexes
SELECT indexrelname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexrelname NOT LIKE '%_pkey';

Maintenance philosophy: Index optimization is not a one-time activity. It is an ongoing operational responsibility .

Part 4 — CQRS and the Read/Write Separation

ERP systems have fundamentally conflicting access patterns:

  • Write path: Highly normalized, transactionally correct, ACID-compliant

  • Read path: Denormalized, aggregated, optimized for reporting

CQRS (Command Query Responsibility Segregation) addresses this .

The CQRS Pattern in ERP

┌─────────────────────────────────────────────────────────────────┐
│                        COMMAND SIDE                             │
│  (Write Operations: CreateOrder, PostJournalEntry, AdjustStock) │
│                                                                 │
│  Domain Model → Business Rules → Event Store / Write DB         │
│                                                                 │
│  Characteristics:                                               │
│  - Full domain logic validation                                 │
│  - Transactional consistency                                    │
│  - Normalized schema                                            │
│  - Event emission for downstream consumers                      │
└─────────────────────────────────────────────────────────────────┘
                              │
                              │ Domain Events
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                         QUERY SIDE                              │
│     (Read Operations: Reports, Dashboards, Search)              │
│                                                                 │
│  Read Models → Optimized Views → Query DB / Cache               │
│                                                                 │
│  Characteristics:                                               │
│  - Denormalized for fast reads                                  │
│  - Eventually consistent                                        │
│  - Optimized for specific query patterns                        │
│  - Can use different storage engines                            │
└─────────────────────────────────────────────────────────────────┘

Why This Matters for ERP

Financial reports require aggregating millions of transactions. Running these queries against the transactional database creates lock contention that blocks new transactions.

CQRS enables:

  • Isolation: Reporting queries never block transactional writes

  • Optimization: Each side uses the right data model for its workload

  • Scalability: Read replicas can scale independently 

Part 5 — Avoiding Technical Debt in High-Concurrency Systems

Technical debt in ERP systems is uniquely dangerous because it compounds across every transaction, every report, and every integration point.

The Edge Case Anti-Pattern

The most common source of ERP technical debt: designing for every possible exception .

“Less than 5% of scenarios were driving over 50% of the design complexity. The majority of transactions never touched those paths. But every transaction carried the weight of that design.” 

The fix: Design for the common path. Isolate rare scenarios. Let the minority require human intervention.

WRONG APPROACH:                    RIGHT APPROACH:
─────────────────────────────      ─────────────────────────────
Every transaction processed         Common path: streamlined
through full validation of          automated workflow
all edge cases                      
                                   Rare scenarios: flagged for
Result:                             human review
- 50% of code handles 5%           
  of transactions                   Result:
- Every transaction pays the        - 95% of transactions fast
  performance cost                  - Edge cases handled correctly
- Testing complexity explodes       - Maintainable codebase
- Change becomes dangerous

The Clean Core Principle

For ERP systems that integrate with external platforms (CRMs, e-commerce, BTP extensions):

Keep the core clean. Build extensions outside. Use events for communication. 

PrincipleImplementation
No core modificationsExtension points only
No direct writes into coreAPI-mediated communication
Event-driven integrationAsync, loosely coupled
Clear data ownershipSystem of record defined per domain

This prevents the “shadow ERP” problem — where extensions become so tightly coupled that they effectively replace the core, creating upgrade nightmares .

Part 6 — Architecture in Practice: Request Flow

Understanding how a request flows through the Clean Architecture ERP layers.

Query Flow (Read Operation)

GET /api/finance/journal-entries?dateFrom=2025-01-01

1. API Controller receives request
   └─ Builds GetJournalEntriesQuery

2. Controller sends query via MediatR
   └─ IMediator.Send(query)

3. Application Handler executes:
   ├─ Uses IJournalEntryRepository (interface from Domain)
   ├─ Applies filtering and pagination
   ├─ Maps entities to DTOs
   └─ Returns Result<PagedResult<JournalEntryDto>>

4. Controller converts Result<T> to API response

Command Flow (Write Operation)

text
POST /api/finance/journal-entries

1. API Controller receives request
   └─ Builds CreateJournalEntryCommand

2. Controller sends command via MediatR
   └─ IMediator.Send(command)

3. Application Handler executes:
   ├─ Validates business constraints
   ├─ Creates domain entity via JournalEntry.Create(...)
   ├─ Saves using repository + unit of work
   └─ Returns Result<JournalEntryDto>

4. Controller emits 201 Created

Cross-Cutting Concerns (MediatR Pipeline)

Production Clean Architecture ERP systems register pipeline behaviors in strict order:

1. LoggingBehavior        → Capture request/response
2. PerformanceBehavior    → Measure execution time
3. ValidationBehavior     → FluentValidation rules
4. CachingBehavior        → For queries implementing ICacheableRequest
5. AuditingBehavior       → Who did what, when
6. NotificationBehavior   → Domain event dispatch
7. RetryBehavior          → Transient failure resilience

Why order matters: Validation must run before caching (don’t cache invalid requests). Auditing must run after validation (only audit valid operations). 

Part 7 — When to Use This Architecture (and When Not To)

Use Clean Architecture + DDD for ERP when:

  • ✅ Business rules are complex and change frequently

  • ✅ Multiple teams work on the same system

  • ✅ The system integrates with many external platforms

  • ✅ Long-term maintainability is a priority

  • ✅ Transaction correctness is non-negotiable

  • ✅ The domain has clear bounded contexts

Do NOT use it when:

  • ❌ The application is a simple CRUD interface

  • ❌ The team lacks experience with domain modeling

  • ❌ Speed to market outweighs long-term structure

  • ❌ The domain is not complex enough to warrant the layers

Clean Architecture is an investment. It pays returns in maintainability and scalability. It costs in initial complexity. The question is whether your ERP system will live long enough to collect the returns.

Conclusion — ERP as an Engineering Discipline

ERP systems are not “business software.” They are distributed systems with financial consequences.

The architecture decisions made at the beginning — how domains are bounded, how data flows, how consistency is maintained, how indexes are designed — determine whether the system will:

  • Scale with transaction volume, or collapse under load

  • Evolve with business requirements, or resist change

  • Remain correct under concurrency, or silently corrupt data

  • Be operable for years, or accumulate debt until replacement is the only option

Clean Architecture, Domain-Driven Design, and PostgreSQL engineering are not buzzwords. They are disciplines — each addressing a specific failure mode of enterprise systems.

The goal is not perfect architecture. The goal is architecture that enables the business to operate, scale, and evolve — without the system becoming the bottleneck.

Author’s Note

This article reflects architectural patterns developed while building CoreBiz ERP — a modular enterprise platform handling accounting, inventory, and cross-departmental transactions. For collaboration on enterprise ERP architecture, reach out via the contact page.

Tags:
Write a comment