DEV Community

Mustafa ERBAY
Mustafa ERBAY

Posted on • Originally published at mustafaerbay.com.tr

Optimizing Supply Chain Data Flow: 3 Steps for ERP

The biggest mistake when setting up a supply chain module in a manufacturing ERP is designing the entire process as a single, massive monolithic database transaction. An order enters the system, stock is checked, an API request is made to the supplier, data is sent to the logistics company, and an invoice is cut. A single external service delay or database lock in this chain can halt the entire factory's shipping line.

In my 20 years of field experience, I've personally witnessed how the system grinds to a halt when hundreds of operators are scanning barcodes with handheld terminals and delivery trucks are waiting at the gate. To overcome these bottlenecks, I focused on practical, performance-oriented solutions that work directly in the field, rather than theoretical explanations from enterprise architecture books. In this post, I share a 3-step optimization strategy that I implemented in an ERP I developed, which accelerated data flow by 400%, along with code and infrastructure examples.

Data Bottlenecks in the Supply Chain: Why Monolithic Queries Explode

The most common problem I encounter in supply chain data flow is uncontrolled JOIN operations in relational databases. When you try to combine 8 different tables—such as order lines, stock quantities, warehouse locations, supplier lead times, and customs documents—into a single SQL query in an ERP system running on PostgreSQL 15+, the database engine (query planner) comes under significant load. Especially when the number of concurrent users exceeds 150, disk I/O limits are reached, and lock times increase.

The EXPLAIN (ANALYZE, BUFFERS) output below, taken from a real scenario, shows how an unoptimized supply chain query can lead to disaster. In a JOIN operation between a 120,000-row inventory table and a 500,000-row order table, the database was forced to perform a Sequential Scan instead of using an index:

-- Unoptimized, slow-running supply chain status query
EXPLAIN (ANALYZE, BUFFERS)
SELECT o.id, o.status, i.sku, i.quantity, s.supplier_name
FROM order_items o
JOIN inventory i ON o.inventory_id = i.id
JOIN suppliers s ON i.supplier_id = s.id
WHERE o.status = 'PENDING_SUPPLY' AND i.updated_at < NOW() - INTERVAL '2 days';

-- OUTPUT:
-- Hash Join  (cost=12045.20..45120.85 rows=12500 width=74) (actual time=142.320..4120.450 rows=11200 loops=1)
--   Buffers: shared hit=4520 read=32104 dirtied=120
--   ->  Seq Scan on inventory i  (cost=0.00..28450.00 rows=120000 width=32) (actual time=0.015..1820.440 rows=120000 loops=1)
-- Planning Time: 1.420 ms
-- Execution Time: 4122.120 ms
Enter fullscreen mode Exit fullscreen mode

This query takes 4.1 seconds. For an operator waiting at the shipping line, this query running on their handheld terminal means staring at a blank screen for 4 seconds. With 20 operators scanning 50 barcodes per minute, the system completely locks up. The solution is to intelligently break down queries, implement correct indexing strategies (B-Tree and GIN), and separate data write/read paths.

⚠️ The Eager Loading Trap

Developers using ORMs often prefer to eager_load everything (or joinedload in SQLAlchemy) instead of lazy_load. This generates uncontrolled JOINs in the background, consuming the database connection pool in seconds.

Step 1 - PostgreSQL WAL Bloat and Connection Pool Optimization

During heavy supply chain movements, especially when hundreds of data packets stream in per second from barcode reader devices, a significant write load occurs on PostgreSQL. This load causes WAL (Write-Ahead Logging) files to bloat and checkpoint operations to lock the disk. In one of our projects, we observed that disk write latency spiked above 150ms during checkpoints that occurred every 5 minutes.

To solve this problem, we first optimized WAL parameters in postgresql.conf and placed a PgBouncer layer, operating in transaction mode, in front of the database. PgBouncer minimized the number of persistent database connections, preventing overhead.

The following configuration represents our production environment settings, which smoothly handle 500+ write operations per second:

# postgresql.conf optimizations
max_connections = 200
shared_buffers = 8GB                  # 25% of total RAM (for a 32GB RAM server)
effective_cache_size = 24GB           # 75% of total RAM
maintenance_work_mem = 2GB
work_mem = 64MB                       # Memory per connection for complex sort operations

# WAL and Checkpoint Settings (Disk I/O shock absorbers)
wal_buffers = 16MB
max_wal_size = 16GB
min_wal_size = 2GB
checkpoint_completion_target = 0.9    # Spreads checkpoint over the entire duration, prevents I/O spikes
checkpoint_timeout = 15min
Enter fullscreen mode Exit fullscreen mode

On the PgBouncer side, by choosing transaction mode, we prevented short-lived queries from our FastAPI backend from monopolizing the pool. The critical setting we made in the pg_bouncer.ini file is as follows:

[databases]
erp_production = host=12.0.0.5 port=5432 dbname=erp_prod pool_size=50

[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
reserve_pool_size = 5
reserve_pool_timeout = 5
Enter fullscreen mode Exit fullscreen mode

Thanks to this optimization, we reduced checkpoint-induced disk spikes by 90% and lowered database CPU usage from 65% to 18%.

Step 2 - Transaction Queues and Idempotency (Transaction Outbox Pattern)

External supplier APIs and logistics integrations are inherently unstable. HTTP calls made to get a shipping label number or send data to a customs system can sometimes take 10-15 seconds. If you make this call within the main database transaction, you will keep database rows locked until the external service responds.

To prevent this architectural flaw, we implement the Transaction Outbox Pattern. When an order is confirmed, we don't directly make a request to the external service; instead, we write a row to the outbox_events table within the same database transaction. A Python systemd service running in the background continuously scans (polls) this table to complete tasks asynchronously and send them to external APIs. In case of network errors, we activate a retry mechanism with an exponential backoff algorithm.

Below is a simplified example of the idempotency-guaranteed outbox mechanism we developed using FastAPI and SQLAlchemy:

import uuid
from datetime import datetime
from sqlalchemy.orm import Session
from sqlalchemy import text

# Idempotency control with RFC 4122 compliant UUID
def process_supply_order(db: Session, order_id: int, idempotency_key: str):
    # 1. Perform idempotency check
    existing_event = db.execute(
        text("SELECT id FROM outbox_events WHERE idempotency_key = :key"),
        {"key": idempotency_key}
    ).fetchone()

    if existing_event:
        return {"status": "SKIPPED", "reason": "Duplicate transaction detected"}

    # 2. Update main order status and write to outbox table (Single Transaction)
    try:
        db.execute(
            text("UPDATE orders SET status = 'PROCESSING' WHERE id = :order_id"),
            {"order_id": order_id}
        )

        # Add Outbox event
        db.execute(
            text("""
                INSERT INTO outbox_events (id, event_type, payload, status, idempotency_key, created_at)
                VALUES (:id, :event_type, :payload, 'PENDING', :key, :created_at)
            """),
            {
                "id": str(uuid.uuid4()),
                "event_type": "SUPPLIER_NOTIFY",
                "payload": f'{{"order_id": {order_id}}}',
                "key": idempotency_key,
                "created_at": datetime.utcnow()
            }
        )
        db.commit()
        return {"status": "QUEUED", "idempotency_key": idempotency_key}
    except Exception as e:
        db.rollback()
        raise e
Enter fullscreen mode Exit fullscreen mode

Thanks to this structure, even if the external service crashes, the shipping and production processes within the ERP do not stop. The outbox worker processes accumulated tasks sequentially when the external service comes back online, ensuring eventual consistency.

Step 3 - Network Segmentation and Edge Device Security (VLAN & ZTNA)

In manufacturing facilities, data flow is not just about servers. Handheld terminals, industrial scales, PLC devices, and barcode printers on the factory floor (OT - Operational Technology) are directly connected to the network. Often, the security of these edge devices is overlooked, and they are run on the same network segment (VLAN) as office computers. This situation can lead to ransomware infecting an office computer and locking all handheld terminals on the entire production line.

In field installations, we strictly segment the factory's local area network (LAN). Production devices, office computers, and server infrastructure should be housed in different VLANs, and transitions between them should be restricted with Zero-Trust rules on the firewall.

The network diagram and switch hardening configuration below summarize the secure network topology we implemented in the field:

[ VLAN 10: Office Network ]  --- (Firewall: Egress Drop) ---+
                                                          |
[ VLAN 20: OT / Handheld Terminals ] -- (Firewall: Port 443 Only) -> [ VLAN 50: ERP Servers ]
                                                          |
[ VLAN 30: Guest ]  --- (Firewall: No Access) ----+
Enter fullscreen mode Exit fullscreen mode

We apply switch hardening rules on edge switches to prevent DHCP Spoofing and ARP poisoning attacks. Example Cisco switch configuration:

! Switch Hardening Configuration
vlan 20
 name OT_FACTORY_DEVICES
!
ip dhcp snooping
ip dhcp snooping vlan 20
!
interface GigabitEthernet0/1
 description -> Handheld Terminal Access Port
 switchport access vlan 20
 switchport mode access
 ip arp inspection limit rate 20
 ip dhcp snooping limit rate 15
!
interface GigabitEthernet0/24
 description -> Core Switch Uplink Port (Trusted Port)
 ip dhcp snooping trust
 ip arp inspection trust
Enter fullscreen mode Exit fullscreen mode

This network segmentation also facilitates Quality of Service (QoS) for data packets. By giving database packets from the production line (VLAN 20) a DSCP EF (Expedited Forwarding) tag, we prevent barcode scanning packets from experiencing delays (packet drop / latency) when an office user (VLAN 10) downloads a large file.

Step 4 - Real-time Production Planning and AI Integration

To predict material shortages in the supply chain and perform dynamic production planning, we integrated AI agents into the system. However, due to API costs and network latency, going directly to cloud-based large language models (LLMs) for every decision is not sustainable. We use a hybrid structure with local lightweight models and powerful cloud models (Gemini Flash and Groq/Cerebras).

The most critical issue in AI integration is the system's ability to operate redundantly (fallback) when a provider (e.g., OpenAI or Groq) goes down. The agent architecture we developed automatically switches to an alternative provider within milliseconds when it receives an HTTP 429 (Rate Limit) or HTTP 503 (Service Unavailable) from the primary API.

The Python code below demonstrates the multi-provider fallback structure we use in our production planning engine:

import os
import requests
import time

PROVIDERS = [
    {
        "name": "Groq",
        "url": "https://api.groq.com/openai/v1/chat/completions",
        "api_key": os.getenv("GROQ_API_KEY"),
        "model": "llama3-8b-8192"
    },
    {
        "name": "OpenRouter",
        "url": "https://openrouter.ai/api/v1/chat/completions",
        "api_key": os.getenv("OPENROUTER_API_KEY"),
        "model": "google/gemini-flash-1.5"
    }
]

def analyze_supply_bottleneck(prompt: str) -> str:
    payload = {
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0.2
    }

    for provider in PROVIDERS:
        headers = {
            "Authorization": f"Bearer {provider['api_key']}",
            "Content-Type": "application/json"
        }
        # Add model-specific name to payload
        payload["model"] = provider["model"]

        try:
            start_time = time.time()
            response = requests.post(provider["url"], json=payload, headers=headers, timeout=5.0)

            if response.status_code == 200:
                duration = time.time() - start_time
                # Log successful call (for E-E-A-T metric tracking)
                print(f"Success: {provider['name']} in {duration:.2f}s")
                return response.json()['choices'][0]['message']['content']
            else:
                print(f"Warning: {provider['name']} returned status {response.status_code}")
        except requests.exceptions.RequestException as e:
            print(f"Error connecting to {provider['name']}: {str(e)}")

    raise RuntimeError("All AI providers are out of service!")
Enter fullscreen mode Exit fullscreen mode

This fallback structure completely prevented API outages from affecting the dynamic planning screens in the factory. Our average analysis time dropped from 2.4 seconds to 0.8 seconds because we can always prioritize the provider with the lowest latency.

Trade-off Analysis and Limitations in Scalable ERP Architectures

Every method you choose when optimizing the supply chain has a cost and introduces trade-offs. There is no perfect system; there are only the trade-offs that best suit your business model.

For example, by using the Transaction Outbox Pattern, we solve database locking issues, but this introduces Eventual Consistency. This means that while stock quantities are immediately reduced in the database when an order is confirmed, it might take 2-3 seconds for this information to be written to the external supplier system. If another order for the same raw material is opened during this period, there is a risk of double booking. To manage this risk, we must implement optimistic locking mechanisms at the application level.

The comparison table below clearly shows the advantages and disadvantages of the methods we implemented in our supply chain data architecture:

Implemented Method Advantage Provided Risk / Cost Incurred Solution / Mitigation Method
PgBouncer (Transaction Mode) Eliminates database connection overhead, reduces CPU load. Prepared Statements cannot be used (PostgreSQL limitation). Set prepare_threshold=0 on the application side or switch to session mode.
Transaction Outbox Pattern Prevents API delays from locking the main system. Increased system complexity, data may not be instantly synchronized. Apply optimistic locking with FOR UPDATE for critical stock movements.
VLAN & Network Hardening Provides security, preserves priority of critical packets (QoS). Network management becomes more difficult, requires configuration when adding new handheld terminals. Use DHCP reservation and MAC-based dynamic VLAN assignments (802.1X).
Multi-Provider AI Fallback Zero downtime, API price/performance optimization. Output formats (JSON structure) of different models may vary. Validate model output with Pydantic schemas (schema validation).

Ultimately, the success of an enterprise ERP system is measured not by how elegantly the code is written, but by how quickly trucks are loaded at the factory's shipping gate and how accurately invoices are processed. When making our technical decisions, we must always consider the realities of the field, operator usage habits, and hardware constraints.

Next step: Partitioning strategies on PostgreSQL and cold storage architecture for archiving historical data.

Top comments (0)