acuamitca.com logo ACUAMITCA
~/blog/article

Persisting Data in Acumatica Events – Part 5: Case Studies & Troubleshooting

Acumatica Customization 9 mins read August 25, 2026
 

Welcome to the final part of this series. Over the previous four parts, we've covered everything from the fundamentals of PXCache to complex production patterns. In this concluding part, we'll examine real-world case studies from Acumatica implementations, common troubleshooting scenarios, and lessons learned from the field.

⚡ Key Insight: The best way to master data persistence in Acumatica is to study how experienced developers have solved real-world problems and learn from their challenges and solutions.


Case Study 1: Performance Crisis in a Multi-Company Environment

A large distribution company with multiple legal entities experienced severe performance degradation during order processing. The system would freeze for 30-60 seconds when saving an order.

The Problem

The custom RowPersisting event handler was querying the database for each order line to validate inventory across all companies. With a typical order having 50-100 lines, this resulted in 50-100 database round trips per order.

// ❌ Original inefficient code
protected virtual void _(Events.RowPersisting<SOLine> e)
{
    var line = e.Row;
    
    // BAD: Query executed for EACH line
    var inventory = PXSelect<InventoryItem,
        Where<InventoryItem.inventoryID, Equal<Required<InventoryItem.inventoryID>>>>
        .Select(Base, line.InventoryID); // 1 round trip per line!
    
    if (inventory?.AvailableQty < line.Quantity)
    {
        throw new PXException($"Insufficient inventory for {inventory.InventoryCD}");
    }
}

The Solution

The solution was to batch the inventory queries and use a PXDatabase select to retrieve all inventory data in a single round trip.

// ✅ Optimized code
private List<InventoryItem> _inventoryCache;

protected virtual void _(Events.RowPersisting<SOLine> e)
{
    var line = e.Row;
    
    // Lazy load inventory cache for this transaction
    if (_inventoryCache == null)
    {
        var inventoryIDs = GetInventoryIDsForOrder();
        _inventoryCache = PXSelect<InventoryItem,
            Where<InventoryItem.inventoryID, In<Required<InventoryItem.inventoryID>>>>
            .Select(Base, inventoryIDs.ToArray())
            .Cast<InventoryItem>()
            .ToList();
    }
    
    var inventory = _inventoryCache.FirstOrDefault(i => i.InventoryID == line.InventoryID);
    if (inventory?.AvailableQty < line.Quantity)
    {
        throw new PXException($"Insufficient inventory for {inventory.InventoryCD}");
    }
}

✅ Lesson Learned: Always batch database queries in event handlers. Avoid database round trips inside loops. Use PXDatabase for bulk operations when event logic isn't required.


Case Study 2: Deadlock During Order Approval

A manufacturer experienced periodic deadlocks when approving orders. The system would hang, requiring manual intervention to restart the approval process.

The Problem

The custom Persist override was creating audit records and updating inventory in the same transaction, using the same graph instance. This caused lock escalation and deadlocks with other concurrent operations.

// ❌ Problematic code
[PXOverride]
public void Persist(Action baseMethod)
{
    // BAD: Creating records in the same graph/transaction
    var order = Base.Orders.Current;
    
    // Creating audit record in same graph
    var audit = new OrderAudit { OrderNbr = order.OrderNbr, Status = "Approved" };
    Base.Audit.Insert(audit);
    
    // Updating inventory in same graph
    UpdateInventory(order);
    
    baseMethod(); // All changes saved together
}

The Solution

The solution was to use a separate graph instance for audit records and update inventory in a background process after the transaction was committed.

// ✅ Optimized code
[PXOverride]
public void Persist(Action baseMethod)
{
    // Use a different graph for audit (created in baseMethod)
    baseMethod();
}

protected virtual void _(Events.RowPersisted<SOOrder> e)
{
    if (e.TranStatus != PXTranStatus.Completed) return;
    
    // Use a separate graph for audit
    using var auditGraph = PXGraph.CreateInstance<AuditGraph>();
    var audit = new OrderAudit { OrderNbr = e.Row.OrderNbr, Status = "Approved" };
    auditGraph.Audit.Insert(audit);
    auditGraph.Actions.PressSave();
    
    // Schedule inventory update for background processing
    QueueInventoryUpdate(e.Row);
}

✅ Lesson Learned: Avoid creating records in Persist overrides that use the same graph. Use RowPersisted with a separate graph instance or schedule long-running operations for background processing.


Case Study 3: Silent Data Corruption in Shipment Processing

A logistics company discovered that shipment quantities were being silently modified during processing, causing inventory discrepancies and customer complaints.

The Problem

The custom RowPersisting handler was modifying the shipment line quantity without raising the appropriate events, leading to inconsistent data in dependent caches.

// ❌ Problematic code
protected virtual void _(Events.RowPersisting<ShipmentLine> e)
{
    var line = e.Row;
    if (line.ShippedQty > line.OrderQty)
    {
        // BAD: Direct field assignment - events not raised
        line.ShippedQty = line.OrderQty; // No events triggered!
    }
}

The Solution

The solution was to use PXCache.SetValueExt which properly raises events and updates dependent caches.

// ✅ Corrected code
protected virtual void _(Events.RowPersisting<ShipmentLine> e)
{
    var line = e.Row;
    if (line.ShippedQty > line.OrderQty)
    {
        // GOOD: SetValueExt raises events and updates status
        e.Cache.SetValueExt<ShipmentLine.shippedQty>(line, line.OrderQty);
        
        // This ensures all dependent logic runs correctly
    }
}

⚠️ Critical Lesson: Never assign values directly in event handlers unless you're absolutely certain no dependent logic exists. Always use SetValueExt to ensure events are raised and caches are updated properly.


Case Study 4: Upgrade Failure with Custom Fields

An ISV solution failed during upgrade from 2025 R2 to 2026 R1 because custom fields didn't have the Usr prefix.

The Problem

Custom fields were added to standard DACs without the Usr prefix, and the customization project used direct SetValue calls without proper event handling.

// ❌ Problematic code
public class SOOrderExt : PXCacheExtension<SOOrder>
{
    // BAD: Missing Usr prefix - may be removed during upgrade
    [PXDBDecimal(2)]
    public virtual decimal? CustomDiscount { get; set; }
}

// In event handler
protected virtual void _(Events.RowUpdating<SOOrder> e)
{
    // BAD: Direct assignment without events
    e.Row.GetExtension<SOOrderExt>().CustomDiscount = CalculateDiscount(e.Row);
}

The Solution

The solution was to add the Usr prefix to all custom fields and use SetValueExt for all field updates.

// ✅ Corrected code
public class SOOrderExt : PXCacheExtension<SOOrder>
{
    // GOOD: Usr prefix ensures upgrade safety
    [PXDBDecimal(2)]
    [PXUIField(DisplayName = "Custom Discount")]
    public virtual decimal? UsrCustomDiscount { get; set; }
}

// In event handler
protected virtual void _(Events.RowUpdating<SOOrder> e)
{
    // GOOD: SetValueExt raises events and maintains data consistency
    e.Cache.SetValueExt<SOOrderExt.usrCustomDiscount>(
        e.Row, 
        CalculateDiscount(e.Row));
}

✅ Lesson Learned: Always use the Usr prefix for custom fields to ensure upgrade safety. Always use SetValueExt instead of direct assignment to maintain data consistency and event integrity.


Troubleshooting Common Errors

Error 1: "Object reference not set to an instance of an object" in CstPageData.Save

This error occurs when a customization package contains ASPX page customizations for screens delivered by another package, and the prerequisite package hasn't been published yet.

Solution: Publish prerequisite packages before importing dependent ones. The import order matters in 2026 R1.

Error 2: "Cannot insert duplicate key" during RowPersisted

This error typically occurs when creating related records in RowPersisted without checking if they already exist.

Solution: Always check for existing records before inserting:

protected virtual void _(Events.RowPersisted<SOOrder> e)
{
    if (e.TranStatus != PXTranStatus.Completed) return;
    
    var order = e.Row;
    // Check if record already exists to avoid duplicates
    var existing = PXSelect<OrderAudit,
        Where<OrderAudit.orderNbr, Equal<Required<OrderAudit.orderNbr>>>>
        .Select(this, order.OrderNbr);
    
    if (existing != null) return;
    
    // Create audit record
}

Error 3: "System.StackOverflowException" in Event Handlers

This error occurs when event handlers modify data in a way that triggers the same event recursively.

Solution: Use a flag to prevent recursion:

private bool _isProcessing = false;

protected virtual void _(Events.RowUpdating<SOOrder> e)
{
    if (_isProcessing) return;
    
    _isProcessing = true;
    try
    {
        var order = e.Row;
        if (order.DiscountPercent > 0)
        {
            e.Cache.SetValueExt<SOOrder.discountAmt>(
                order,
                order.TotalAmount * order.DiscountPercent / 100);
        }
    }
    finally
    {
        _isProcessing = false;
    }
}

Final Best Practices Checklist

✅ Use Usr Prefix for Custom Fields

Always prefix custom fields with "Usr" to ensure upgrade safety.

✅ Use SetValueExt for All Field Updates

Avoid direct assignment. Let events propagate correctly.

✅ Batch Database Operations

Never query the database inside loops. Use batch operations.

✅ Use Separate Graph Instances

Use PXGraph.CreateInstance() for related records to avoid conflicts.

✅ Prevent Recursion

Use flags or status checks to avoid infinite event loops.

✅ Test with Production Data

Always test customizations with production-like data volumes to catch performance issues.


Conclusion

📌 Series Wrap-up: This five-part series has covered the complete spectrum of data persistence in Acumatica events:

  • Part 1: PXCache foundations and data modification methods
  • Part 2: Persisting within specific events and advanced techniques
  • Part 3: Validation, cross-table persistence, and performance optimization
  • Part 4: Complex scenarios and production-ready patterns
  • Part 5: Real-world case studies and troubleshooting

⚡ Final Thought: The key to successful Acumatica customization is understanding the framework's event model and persistence patterns. By following the principles and practices outlined in this series, you can build robust, maintainable customizations that stand the test of time.

🔍 Next Steps: Apply these patterns in your own projects. Start with simple customizations and gradually incorporate more complex techniques. The Acumatica Developer Network provides additional resources and community support.