Skip to content
← BACK TO IMPACT & RESULTS

[ CAPABILITY / 01 ] · ENGINEERING INSIGHT

The boundaries that make enterprise systems maintainable

Designing ERP workflows around ownership, organizational scope and the business events that keep data consistent.

BY JAYASRIRAAM S8 MIN READ
ERPTypeScriptPostgreSQLArchitecture

[ 01 ]

Start with the business boundary

An ERP system rarely fails because a single form is complicated. It becomes difficult when a purchasing change affects stock, a branch-specific approval affects finance, and a correction must remain explainable months later. The architecture has to preserve those relationships without allowing every module to rewrite everybody else’s data.

I approach inventory, finance, academic management and administration as distinct domains with explicit owners. A domain owns the rules behind its state, not just a set of tables. Inventory can expose a stock movement; finance can record its accounting consequence. That is more durable than letting a controller update two unrelated tables because the fields happen to be available.

The first useful design artifact is often a map of commands, owners and consequences. What action changes a quantity? Who may approve it? Which downstream record must exist before the workflow is considered complete? Those questions expose the boundaries before a module structure becomes a permanent constraint.

[ 02 ]

Make organizational scope explicit

A multi-organization product needs more than an organization selector in the interface. Each request must resolve the authenticated actor, the organization, the permitted branches and the intended action. The service performing a write checks that context against the resource being changed; a hidden menu item is not an authorization boundary.

Organization-wide policies and branch assignments are different dimensions. A finance user may see consolidated reports without having authority to approve transactions at every branch. Model those decisions explicitly, and fail closed when scope is missing or ambiguous. Background workers and exports must carry the same scope as interactive requests.

Separate databases per tenant can simplify isolation and specialized maintenance, but increase migrations, connections and operational overhead. Shared tables are easier to operate at smaller scale, provided tenant keys, indexes and tests consistently enforce isolation. The correct choice depends on compliance, noisy-neighbor behavior and the team’s ability to operate it.

[ 03 ]

Model the transaction, not just the screen

Stock balances are easier to reason about when they derive from movements with a source, destination, quantity and reason. A receiving transaction can persist the document and its inventory entries atomically in one database. A retry should use a stable operation key so that a network timeout does not create a second receipt.

External services do not participate in that database transaction. A payment gateway or courier call can succeed while the application loses its response. Record an internal operation first, dispatch it with a durable outbox or job, and reconcile provider callbacks against a verified reference. Treat callbacks as potentially duplicated and arriving out of order.

TYPESCRIPT / ILLUSTRATIVE
type Scope = { organizationId: string; branchIds: readonly string[] };

function requireBranch(scope: Scope, organizationId: string, branchId: string) {
  if (scope.organizationId !== organizationId || !scope.branchIds.includes(branchId)) {
    throw new Error('Branch access denied');
  }
}
Illustrative domain guard: call it with scope derived from authenticated server-side membership, never from client-supplied permissions.

[ 04 ]

Choose consistency deliberately

Within a single transactional boundary, write the invariant and its audit record together. Across boundaries, publish a durable event only after the originating change commits. A consumer should be idempotent and should record the event identifier it processed; delivery is commonly at least once, not magically exactly once.

A dashboard can tolerate delayed projections more readily than an approval that spends a budget. Identify which decisions require current authoritative data and which can read a projection. If a projection is stale, show its freshness or provide a path to the authoritative record rather than presenting a lagging number as immediate truth.

Compensating actions are business processes, not automatic rollbacks. Reversing an invoice may require a credit note and audit trail rather than deleting the original row. Define who can perform that action, which state transitions are valid and how downstream systems learn about it.

[ 05 ]

Keep change and operations affordable

A modular monolith is often a better starting point than a distributed service per business concept. It preserves transaction boundaries and makes local debugging straightforward, while disciplined module interfaces keep later extraction possible. Split a service when independent scaling, ownership or isolation justifies the operational cost, not merely because the diagram looks cleaner.

Index the queries actually used by organization, branch, status and time; inspect execution plans with realistic cardinality. A composite index that supports one report may slow writes or fail to help another. Include authorization filters when profiling, because a fast unscoped query is not the query the application should run.

Test the failure paths alongside happy-path workflows: conflicting approvals, branch reassignment, duplicate webhook delivery, partial imports and retries after a timeout. Log correlation identifiers and business operation identifiers without logging credentials or sensitive payloads. An auditable system lets an operator reconstruct what happened without editing production data by hand.

The most useful module boundary is the one that protects a business invariant, not the one that makes a directory tree look tidy.

[ 06 ]

Workflow states carry business meaning

Consider a purchase request that becomes an approved order, then a receipt and finally a payable. Those are related records, but they are not interchangeable statuses on one mutable object. Approval authorizes a commitment; receiving records what actually arrived; an invoice records a claim to be paid. Partial deliveries and quantity differences are normal cases, not exceptions to be patched into an otherwise linear flow.

Represent transitions as operations with preconditions. For example, an order cannot be received beyond its permitted tolerance without an explicit exception, and a posted receipt should not be silently edited after a financial record references it. A transition can record the actor, timestamp, reason and source document in the same transaction. The state history is then useful to both an operator and an auditor.

Design reversal alongside posting. A correction might create an adjusting movement or a credit document rather than removing history. The exact policy belongs to the organization and its accounting requirements; the engineering responsibility is to make the policy enforceable and the resulting history intelligible.

[ 07 ]

Integrations need a source of truth

A payment provider and an internal ledger answer different questions. A provider can report that a payment was captured, while the application still has to associate that event with an authorized order, reconcile the amount and currency, and decide how the business document changes. Verify provider signatures using the documented scheme, look up the server-owned payment reference, and persist the event before acknowledging it.

For a courier integration, shipment creation, label generation and delivery status may each fail independently. Store the provider reference and the last verified status, but do not assume that a single request timeout means no shipment was created. Where the provider supports idempotency, use it; otherwise reconcile by a suitable external reference before creating a replacement. A manual review queue is a legitimate outcome when the answer remains uncertain.

Razorpay and Shiprocket are examples from the project context, not evidence that every safeguard described here was deployed. Their current APIs and signature requirements must be checked against their own documentation during implementation. Provider-specific adapters keep those details from becoming assumptions throughout the domain model.

[ 08 ]

Migration and reporting are part of the model

Enterprise data rarely arrives clean. An import should separate validation from commitment: inspect the file, report row-level problems, identify duplicate external references, and allow a controlled retry. A dry run is useful only if it applies the same validation rules as the actual write. Record the import batch and the origin of each resulting record so a later correction can be traced.

Reports often cross module boundaries. Rather than allowing every report to bypass ownership and join arbitrary tables, define a documented reporting projection with its own freshness expectations and access policy. Historical reports may need the organizational hierarchy as it existed when the transaction occurred, not only the current branch name or manager assignment.

Schema changes also need a deployment sequence. Add a compatible field, deploy code that can handle both old and new representations, backfill in observable batches, and remove the old path only after consumers have moved. A migration is not complete until rollback or forward-repair procedures have been considered and a restore has been tested.

Operational ownership matters after release. Name a person or team responsible for a failed import, a stuck payment reconciliation and a report that no longer agrees with its source. Give that owner a documented procedure and a way to see the relevant operation without granting unrestricted database access. These routines determine whether a carefully modeled system remains maintainable in practice.

[ 09 ]

The architecture should explain the work

School and enterprise workflows make the same demand: the software must represent authority, history and exceptions, not just successful submissions. A well-designed model makes the common path fast while giving unusual cases a controlled, traceable route.

I favor small explicit contracts, consistent tenant scope and recoverable integrations over clever generic abstractions. They make it easier for the next engineer to change a policy without accidentally changing the meaning of existing records.

[ FIELD NOTES ]

Key takeaways

  1. Give each domain ownership of its invariants.
  2. Enforce organization and branch scope on the server.
  3. Make external side effects retryable and reconcilable.
  4. Use audit trails and realistic failure tests as design inputs.

RELATED TECHNOLOGIES

ERPTypeScriptPostgreSQLArchitecture