OutSystems Delivery Specialist study materials
OutSystems Delivery Specialist study resources

The dashboard that takes nine seconds

OutSystems Delivery Specialist · Put your knowledge to work in context.

All case studies

The dashboard that takes nine seconds

A reactive dashboard is used by 1,200 field agents. The main list shows open orders from a 180,000-row Order table joined to Customer. Each row displays the customer credit status, which a data action fetches per row, so a 1,000-row page issues 1,000 server round trips. The search box filters with the Contains operator on the customer name and re-fetches on every keystroke. Aggregate Max Records is set to 1,000 so that 'the export and the screen behave the same'. Sorting on the computed Total value column re-sorts the fetched list in the browser. The selected customer's full order history, up to 3,000 records, is kept in a client variable so the screen 'does not refetch'. Median load time is 9 seconds, p95 is 22 seconds, and database CPU peaks at 85 percent during working hours.

1. Rank the causes by impact and justify the order of fixing them.

Read the worked answer

Rank by latency removed per unit of risk, not by how wrong each one feels.

  1. The per-row data action (N+1). 1,000 round trips per page dwarfs everything else: each is a network hop plus a database query, so cost scales linearly with page size and every page-size increase makes the screen worse. Fix by joining the needed Customer attribute into the main aggregate and returning it as a column - one query instead of 1,001.
  2. Contains on a non-indexed column with a 1,000-row fetch per keystroke. Contains compiles to a leading-wildcard LIKE, which cannot use an index, so the database scans. Combined with a fetch on every keystroke, typing 'Smith' costs six full scans. Fix with Starts With (index-friendly) or a deliberate full-text/SQL approach, plus debounce, plus a page size that matches what the user actually sees.
  3. Sorting and total computed in the browser over 1,000 rows. This is a client CPU and memory cost, and it also gives wrong results once paging is real, because you can only sort what you fetched.

Why this order: items 1 and 2 are server-side and reduce database CPU; item 3 is a correctness problem that disappears once filters, sorting and paging are pushed to the server. Doing 3 first would optimize a page that is about to be redesigned.

2. Describe the replacement fetch strategy in concrete terms: filtering, paging, total count and sorting.

Read the worked answer

One parameterised query per user interaction.

  • Filter: pass the search term as an input parameter to a server-side action; the aggregate filters on Customer.Name with Starts With (falling back to an indexed keyword column or a hand-written SQL query with a validated parameter if substring search is genuinely required). Never concatenate the term into SQL.
  • Paging: set Max Records to the page size (25) and drive Start Index from the page number. A 1,000-row Max Records is not an optimization, it is a download.
  • Total count: a second aggregate on the same filters using Count with Max Records 1. Run it only when the filter changes, not on every page step; that removes a scan per page while keeping '1 to 25 of 3,412' honest.
  • Sorting: an aggregate cannot take a dynamic ORDER BY. Either expose a small validated set of static variants, or use a SQL query whose sort column is chosen from a whitelist so it cannot become an injection point. Client-side sorting is only acceptable on a single already-fetched page.
  • Round trips: merge the credit status into the main query, and cancel or ignore stale responses when the user keeps typing (debounce around 300 ms) so an older slower response cannot overwrite a newer one.

Expected effect: one query per interaction instead of 1,001, an index-friendly filter, and a page size the user can actually read. Measure with the screen's server timings before and after rather than trusting the perceived speed.

3. The selected customer's 3,000-record history sits in a client variable. What is wrong with that and what would you do instead?

Read the worked answer

Client variables are serialized into the browser. Every record in that list is downloaded, parsed into memory, and re-serialized on each round trip that carries the variable. At 3,000 rows this is a large payload per request, it grows with the session, and nothing tells the developer when it becomes the bottleneck. It is also data the user can read in the browser, which is fine for order history but not for anything sensitive that was only 'hidden' in the UI.

Replace state with a key. Keep the selected customer id - a scalar - and fetch the history when the user opens the section, with paging and a server-side filter. The list is then bounded by what is displayed, and stale data is impossible because the fetch follows the selection.

If a cache is genuinely needed, put it server-side with an explicit lifetime and an explicit invalidation point (for example a cached structure refreshed when an order is written), rather than in the browser. Session variables are also serialized per request, so they are not a free upgrade from client variables - they move the cost, they do not remove it. The decision rule: cache counts and small structures with a known invalidation event; never cache unbounded row sets in session state.