An ERP backend becomes difficult to scale when every business action triggers a chain of synchronous API calls. A purchase order may update inventory, create accounting entries, notify procurement, refresh dashboards, and invoke external integrations in the same request lifecycle. As transaction volume grows, one slow dependency can hold the entire workflow open.
This is where ERP Development Services need an architectural approach beyond simply adding more application servers. A better pattern is to separate immediate operations from background work using queues, events, idempotent consumers, and independently scalable workers.
For teams planning custom ERP development, this approach provides a practical way to keep transactional APIs responsive while still processing complex business workflows reliably.
Context and Setup
The architecture discussed here fits an ERP system with modules such as orders, inventory, procurement, finance, and reporting.
A typical deployment can look like this:
Client
|
v
API Gateway
|
v
Node.js ERP API
|
+---- PostgreSQL
|
+---- Event Queue
|
+---- Inventory Worker
+---- Accounting Worker
+---- Notification Worker
+---- Reporting Worker
The important distinction is between transactional work and derived work.
Creating an order and validating its core business rules may need to happen synchronously. Generating a notification, updating an analytics projection, or starting a downstream synchronization job usually does not.
AWS recommends loosely coupling distributed components and using asynchronous communication where an immediate response is unnecessary. Queue-based processing can also allow consumers to scale independently from producers.
There is also a practical reason to use familiar infrastructure. The 2024 Stack Overflow Developer Survey reported that Docker was used by 59% of professional developers, while PostgreSQL was used by 49% of respondents and remained the most-used database in that survey.
Building ERP Development Services Around Asynchronous Processing
Step 1: Define the transaction boundary
The first step in ERP Development Services is deciding what must succeed before the API returns.
For example, an order creation request might require:
- Validate customer and product data.
- Check pricing and authorization rules.
- Create the order.
- Reserve inventory.
- Commit the database transaction.
- Publish an
OrderCreatedevent.
Email delivery, reporting updates, external synchronization, and audit enrichment can happen afterward.
This distinction prevents a common architecture problem: keeping a database transaction open while waiting for unrelated external systems.
A useful rule is:
If the caller only needs confirmation that the business action was accepted, move downstream processing out of the request path.
Step 2: Publish domain events safely
A Node.js service can expose an API that creates the transaction and publishes work for downstream consumers.
app.post("/orders", async (req, res) => {
const order = await createOrderTransaction(req.body);
await queue.send({
type: "OrderCreated",
orderId: order.id
});
// Why: the client does not wait for reporting or notifications.
return res.status(201).json({
id: order.id,
status: "accepted"
});
});
In production, simply writing to the database and then publishing an event can introduce a failure window. The database commit may succeed while the queue operation fails.
A stronger implementation uses an outbox pattern:
Database Transaction
|
+-- orders
|
+-- outbox_events
|
v
Event Publisher
|
v
Message Queue
The order and its event are committed together. A background publisher then reads unpublished events and sends them to the queue.
This gives the system a recoverable path when message delivery fails.
Step 3: Make workers idempotent
The third step in ERP Development Services is protecting consumers from duplicate messages.
Queues and retry mechanisms can result in the same event being processed more than once. An accounting worker, for example, must not create two invoices because it received the same OrderCreated event twice.
One simple pattern is to maintain a processed-event table:
async function processEvent(event) {
const alreadyProcessed = await hasProcessed(event.id);
if (alreadyProcessed) {
return; // Why: prevents duplicate business operations.
}
await database.transaction(async (tx) => {
await createAccountingEntry(tx, event);
await markProcessed(tx, event.id);
});
}
The trade-off is additional database state and transaction handling. However, this is generally preferable to relying on the assumption that every message will arrive exactly once.
AWS also recommends designing systems around failure isolation and limiting unnecessary dependency calls because chatty synchronous interactions can increase coupling and latency.
Real-World Application
In one of our ERP projects at Oodles, TimeForge involved employee scheduling, attendance, and time-off management, with integration into retail and restaurant point-of-sale systems. The engineering work included upgrading the Spring framework from version 2.0 to 4.0 and optimizing query execution, shift assignment, employee onboarding, and page-loading performance. The measurable delivery outcome was the framework upgrade itself, combined with targeted optimization of these operational workflows.
Another Oodles ERP implementation for DLB integrated sales, purchasing, inventory, accounting, and warehouse operations through Odoo, with APIs connecting the ERP to external systems. This illustrates why ERP architecture must treat integrations as explicit system boundaries rather than embedding every dependency directly into transactional workflows.
You can explore more of the engineering work and technical capabilities delivered by Oodles.
Key Takeaways
- ERP Development Services should separate transactional operations from asynchronous business workflows.
- Use database transactions for business-critical state changes, then publish events for downstream processing.
- The outbox pattern reduces the risk of losing events between database commits and message publication.
- Idempotent consumers are essential when queues and retries can deliver duplicate messages.
- Queue-based architecture lets inventory, accounting, reporting, and notification workers scale independently.
- Measure actual bottlenecks before introducing additional services. More components do not automatically mean better performance.
Have a different approach to designing asynchronous ERP workflows, or a production issue you've encountered with queues and event processing? Share it in the comments. For technical discussions around ERP Development Service*s*, you can also contact us.
FAQ
1. What are ERP Development Services?
ERP Development Services involve designing, building, integrating, customizing, and maintaining enterprise resource planning software around specific business workflows. They can include modules for finance, inventory, procurement, manufacturing, workforce management, reporting, integrations, APIs, and workflow automation.
2. Why use event-driven architecture in ERP systems?
Event-driven architecture allows ERP modules to communicate without forcing every operation into the same synchronous request. This is useful when actions such as notifications, reporting updates, integrations, or background calculations do not need to complete before the user receives an acknowledgment.
3. What is the outbox pattern in ERP architecture?
The outbox pattern stores a business event in the same database transaction as the business change. A separate publisher later sends that event to a queue or event broker. This reduces the risk of a committed ERP transaction having no corresponding downstream event.
4. How do ERP workers prevent duplicate processing?
ERP workers can prevent duplicate processing by assigning every event a unique identifier and storing successfully processed identifiers. Before performing a business operation, the worker checks whether the event was already handled. Database transactions can make the check and business update atomic.
5. When should ERP Development Services use synchronous APIs?
ERP Development Services should use synchronous APIs when the caller needs an immediate result, such as validating credentials, checking authorization, confirming inventory availability, or creating a core transaction. Long-running or non-critical downstream tasks are usually better suited to asynchronous processing.
Top comments (0)