Diagnosing a Slow Order History Screen at Peak
An online retailer's OutSystems Reactive Web order portal serves 1,200 concurrent users at peak. The order history screen returns the last 24 months for a customer. Its aggregate reads a 1.2M-row Order entity, filters by CustomerId, sorts by CreatedOn descending, and has Max Records set to 0 and Fetch set to At Start. A separate aggregate counts all orders for the same customer. The screen then calls a server action that loops through each order and calls a second aggregate for shipment status. p95 response time is 8.2 seconds; database CPU reaches 96%; application threads queue. There is no index on CustomerId+CreatedOn. The team added an in-memory cache for product categories, which improved the home page but not this screen. Peak traffic doubles next quarter. The tech lead must fix the screen and set a sustainable performance approach.
1. Diagnose the main causes and rank them by impact.
Read the worked answer
The screen is doing three expensive things at once. Highest impact: Max Records 0 with Fetch At Start returns every order for the customer and loads them into the screen; for a long-tenured customer this can be thousands of rows, repeated per request. Second: the loop calls a shipment aggregate per order, an N+1 pattern; 2,000 orders become 2,001 queries. Third: the missing composite index on CustomerId+CreatedOn forces a scan and sort on 1.2M rows; the separate count aggregate doubles the work. Fourth: no server-side pagination means the UI cannot limit data. The category cache is irrelevant because the bottleneck is order data and query count. Rank by expected reduction: remove N+1 and cap records first, add the index second, combine or replace the count third. Use the database execution plan and OutSystems logs to confirm before changing. Do not start by scaling the app tier; that hides the query pattern and increases database load.
2. Redesign the screen and data access. Be specific about OutSystems constructs.
Read the worked answer
Use a server-side paginated data action. Set the aggregate Max Records to 50 or 100 and Fetch to Only on Demand; pass CustomerId and a page cursor or offset. Sort by CreatedOn descending and use the composite index. Replace the per-order shipment aggregate with a single aggregate that joins Order and Shipment, or a batch fetch by order ID set, then map in memory. Avoid a separate count per page: either show load more without a total, or compute a count once per customer and cache it briefly with invalidation on new order. Expose the data through a data action used by the screen; keep the preparation light and move optional calls to on-demand. If a SQL widget is needed for a covering query, keep parameters bound and verify the plan. Add a performance budget: p95 under 2 seconds at 1,200 users. Test with a customer who has 5,000 orders, not the seeded test user with 10.
3. How do you prepare for doubled peak traffic and prevent recurrence?
Read the worked answer
Load test the order history and checkout paths at 2,400 concurrent users with realistic data distribution. Instrument the screen and aggregates; monitor p95, database CPU, slow queries, thread pool and error rate. Capacity: add a read replica only if reads are the bottleneck and the driver supports it; scale the app tier horizontally; move noncritical work such as email and export to timers or async. Add caching only for stable reference data, with explicit invalidation. Put a query review gate on any aggregate over a large entity: max records, index, filter, and expected cardinality. In LifeTime, create alerts on database CPU above 80% and p95 above 3 seconds. Do a monthly performance regression test. The goal is not one fast screen; it is a rule that no production screen can fetch unbounded rows or loop a query. Track the number of N+1 patterns found in code review as a leading metric.