On this page

A concurrent customer dashboard

The registered customer has an identifier. A dashboard now needs that customer, order history and balance information. Those independent reads can overlap. Within each branch, a later step still waits for the preceding step's result.

The customer repository is the one already built in the walkthrough. The host supplies an HttpClient whose BaseAddress points to its account gateway. The two HTTP routes below are contracts for that gateway; this example does not implement the remote service. The host also owns a shared request Gate and keeps it alive until all dashboard users finish.

The result and client

import Global.System
import Global.System.Collections.Generic
import Global.System.Net.Http

public class CustomerDashboard
    public readonly Customer as Customer
    public readonly OrdersJson as String
    public readonly BalanceJson as String

    public constructor(customer as Customer, orders as String, balance as String)
        self.Customer = arguments.customer
        OrdersJson = orders
        BalanceJson = balance
    end constructor
end class

public class CustomerDashboardClient
    private readonly customers as CustomerRegistrationService
    private readonly http as HttpClient
    private readonly requests as Gate

    public constructor(service as CustomerRegistrationService, client as HttpClient, requestGate as Gate)
        require service is not nothing
        require client is not nothing
        require requestGate is not nothing
        customers = service
        http = client
        requests = requestGate
    end constructor

    public async action LoadAsync(customerId as Guid)
        returns CustomerDashboard
        require customerId <> Guid.Empty
        concurrent all
            returns customer as Customer, orders as String, balance as String
            customer = run
                let found = await customers.FindAsync(customerId)
                if found is nothing then
                    throw new KeyNotFoundException("Customer was not found")
                end if
                return found
            end run

            orders = run
                enter requests
                    let json = await http.GetStringAsync("customers/{{customerId}}/orders", Cancellation.Token)
                    return json.Trim()
                leave
            end run

            balance = run
                enter requests
                    return await http.GetStringAsync("customers/{{customerId}}/balance", Cancellation.Token)
                leave
            end run
        end concurrent
        return new CustomerDashboard(customer, orders, balance)
    end action
end class

The orders branch trims its response as soon as that request finishes. It does not wait for the balance branch. A branch return completes its own body after required cleanup; it does not return from LoadAsync. The owning scope joins all producers before publishing the three values. No sibling can read another's unpublished result.

This example needs all three results, so a missing customer or failed remote request fails the dashboard. One producer failure propagates unchanged; several eligible failures become AggregateException in source order. The scope still joins its owned work. A try around LoadAsync can handle that outcome; a try inside a branch is appropriate only when the application has a real fallback for that branch.

Lifetime and policy choices

Create one Gate(4) for gateway requests shared across dashboard clients. The Gate limits requests across callers; it does not change the HttpClient's native request contract. It also does not automatically make unrelated mutable application state safe. Both the Gate and HttpClient are borrowed by the client and disposed by their owner after active dashboards settle.

The registration service remains scoped. Do not store a request-scoped service in a singleton dashboard client. This repository creates a separate connection per call; never parallelize several operations through one shared DbContext or live reader just because the syntax permits overlap.

This is not a cross-service transaction or a point-in-time snapshot. If access to orders depends on the customer lookup succeeding, perform that lookup and its access check before launching the HTTP branches. Running all three immediately is appropriate only when the host has already authorized the identifier and speculative independent reads are acceptable.

Newly registered customers remain inactive. A read-only dashboard does not activate them, and it does not bypass the separate order workflow's active-customer requirement.

For a list of customers, the bounded dashboard batch recipe adds concurrent for_each and source-ordered collect. The same shared Gate still limits total outgoing requests.

Previous: checks and changes. Next: queued reports.