Designing a Real-Time Net Worth & Portfolio Valuation System
A complete system design walkthrough: aggregating holdings across stocks, crypto, real estate, cash, and liabilities into one live, always-accurate number — at millions of requests per minute.
Introduction & History
“Net worth” is a simple idea: everything you own, minus everything you owe. But turning that simple idea into a live number on a phone screen is one of the harder real-time data engineering problems in consumer finance. A person’s wealth today is rarely sitting in one place. It is spread across a brokerage account holding stocks and ETFs, a 401(k) or retirement account, a couple of bank accounts, a crypto exchange wallet, a mortgage, a car loan, and maybe a manually tracked house valuation or a piece of jewelry. Each of those pieces changes value at a different speed — bank cash barely moves, stock prices move every second the market is open, crypto prices move every second of every day, and real estate value moves over months.
The idea of aggregating financial accounts into one dashboard is not new. Personal finance tools in the 2000s popularized the concept of linking bank and brokerage accounts through data aggregators, and category-defining products in the 2010s (Personal Capital, Mint, and later apps like Kubera, Empower, and Copilot) turned “net worth tracking” into a mainstream feature rather than a spreadsheet hobby. What has changed over the last decade is the expectation of the user: they no longer want a number that updates once a day after a batch job runs overnight. They want to open the app during a market swing and watch their portfolio value move in near real time, the same way they’d watch a stock ticker.
This tutorial designs that system from the ground up: a platform that continuously ingests holdings from many financial institutions and asset classes, continuously ingests live market prices, computes the value of every position, and rolls all of it up into a single, always-fresh net worth figure — pushed to the client the moment something meaningful changes, at a scale of millions of requests per minute across a large user base.
“Why is real-time net worth harder than a normal e-commerce dashboard?” Because the data source is federated (many external institutions, each with different APIs, rate limits, and reliability), the pricing data is a continuous stream rather than a request-response fact, and correctness matters — this is money, and small aggregation bugs erode user trust immediately.
Problem & Motivation
Let’s define the problem precisely before drawing any boxes.
2.1 What we are building
A platform-level system that, for any given user, can answer the question “what is my net worth right now?” in well under a second, where “right now” means the number reflects the latest known market prices and the latest known holdings, and where the number updates live on the client without the user refreshing the page.
2.2 Why This Is Hard
Different valuation rules
Public equities and ETFs are valued as quantity × last traded price. Crypto is valued similarly but trades 24/7 across many venues with different prices. Cash is valued at face value, adjusted for currency. Real estate has no live market price — it needs a model-based estimate (an AVM) refreshed periodically. Private assets may only ever have a manually entered value.
Many accounts & institutions
A single user might link eight to twelve external accounts. Each institution exposes data differently — some support real push notifications for balance changes, most only support periodic polling through a data aggregator.
Debts count too
Net worth is assets minus liabilities. Mortgages, credit card balances, student loans, and margin loan balances must be tracked and valued with the same rigor as assets.
Unit normalization
A user might hold USD cash, EUR-denominated bonds, and Bitcoin. Everything must be converted to a single reporting currency using a live exchange rate.
Cost vs. accuracy
Pulling live data from every linked institution on every single price tick would be prohibitively expensive and would violate most institutions’ rate limits. The system has to be smart about what triggers a recomputation.
Fan-out storms
With a large user base and market prices changing many times a second for the most popular symbols (a handful of stocks and cryptocurrencies are held by a huge fraction of users), a naive design would trigger a storm of recomputation on every tick.
2.3 Functional Requirements
- Link external accounts (brokerage, bank, crypto exchange, retirement, loans) securely.
- Continuously synchronize holdings (positions, quantities, cost basis) from linked accounts.
- Continuously ingest live prices for every held instrument (equities, ETFs, crypto, forex rates).
- Support manually entered assets and liabilities (real estate, vehicles, private equity, jewelry, personal loans).
- Compute per-position market value, per-account value, per-asset-class rollups, and total net worth in the user’s chosen reporting currency.
- Push live net worth updates to connected clients (mobile app, web app) with low latency.
- Persist historical snapshots so the user can see a net worth trend line over time (day, month, year).
- Alert users on large swings or sync failures.
2.4 Non-Functional Requirements
- Scale: tens of millions of registered users, with millions of read/update requests per minute during market hours.
- Latency: a net worth read from cache should return in under 100 ms; a live push update should reach the client within 1–3 seconds of a meaningful price change.
- Consistency: eventual consistency is acceptable for market-driven valuation; strong consistency is required for the underlying holdings ledger (you cannot lose track of how many shares someone owns).
- Availability: the read path (viewing net worth) should stay available even if a downstream data aggregator or a specific institution’s API is down; degrade gracefully to “last known good value” with a staleness indicator.
- Auditability: every valuation must be traceable — which price was used, at what timestamp, from which source.
Architecture & Components
At a high level, the system is split into four planes: an ingestion plane (pulling holdings and prices in from the outside world), a computation plane (turning raw holdings and prices into valuations and rollups), a storage plane (the ledger of truth plus fast read stores), and a delivery plane (getting the number to the client quickly, and keeping it live).
3.1 Account Linking Service
Handles the OAuth or credential-exchange flow when a user connects a new institution. In practice, most platforms do not talk to thousands of banks and brokers directly; they use a financial data aggregator (a third-party service that already has connections into thousands of institutions) as a middle layer, and only build direct API integrations for the highest-volume partners (major brokerages, major crypto exchanges) where richer or faster data is worth the engineering cost.
3.2 Sync Workers
A fleet of workers responsible for pulling the actual holdings data — what does this account contain right now, and how many units of each thing does it hold. Sync workers run on a mix of triggers: webhook notifications from institutions that support them, and scheduled polling for the ones that don’t. Every successful sync writes to the Positions and Accounts DB, which is the ledger of truth for “who owns what, how much of it.”
3.3 Market Data Ingestion Service
Subscribes to live price feeds — for equities and ETFs this is typically a licensed market data vendor; for crypto this can be direct exchange WebSocket feeds; for foreign exchange, a live FX rate provider. Every tick is published onto a streaming bus (Kafka is the natural choice here) as a PriceUpdated event, keyed by instrument symbol.
3.4 Valuation Engine
A stream-processing service (built with something like Kafka Streams or Apache Flink) that is the mathematical heart of the system. It maintains an in-memory (and checkpointed) view of “which positions hold which instrument,” so that when a PriceUpdated event arrives for, say, AAPL, it can instantly recompute the market value of every position across every user that holds AAPL, without querying a database for each one.
3.5 Net Worth Aggregation Service
Consumes valuation-changed events and rolls individual position values up into account totals, asset-class totals, and a single net-worth figure per user, subtracting liabilities and converting everything into the user’s reporting currency. This service intentionally debounces and throttles — a user’s net worth does not need to be recomputed and pushed 40 times a second just because a held stock ticks 40 times a second.
3.6 Liabilities Service
Tracks debts: mortgages, auto loans, credit cards, personal loans, margin balances. Structurally very similar to the assets side, but simpler — most liabilities update on a daily statement cycle rather than a live feed, with the notable exception of margin loan balances, which can change intraday.
3.7 Currency / FX Service
Provides live foreign exchange rates so that holdings in different currencies can be normalized into one reporting currency. This is itself just another kind of “price feed” and can reuse much of the market data ingestion machinery.
3.8 Realtime Push Gateway
Maintains a persistent WebSocket (or Server-Sent Events) connection per active client and delivers net-worth updates as they are produced by the Aggregation Service. It also handles connection scaling, reconnection, and fan-out to multiple devices for the same user.
3.9 Snapshot / Time Series Store
Periodically (and after significant changes) writes an immutable snapshot of a user’s net worth to a time-series-optimized store, which powers historical trend charts (1D, 1W, 1M, 1Y, All).
“Why split Valuation Engine and Aggregation Service into two separate services instead of one?” Because they scale differently and fail differently. Valuation is driven by market tick volume (very high frequency, narrow computation: price × quantity). Aggregation is driven by user activity and needs richer business logic (currency conversion, liabilities, debouncing, per-user rules). Separating them lets you scale the tick-processing hot path independently from the per-user rollup logic, and lets a bug in one not take down the other.
Internal Working
The most important internal design decision in this system is this: never compute net worth by fetching live data on demand. If the system tried to call out to every linked institution and every price feed the moment a user opened the app, response times would be measured in seconds (or would fail outright when an institution’s API is slow), and the system would breach rate limits under real load. Instead, the system follows a continuously-maintained materialized view pattern: prices and holdings stream in constantly in the background, valuations are recomputed incrementally as changes arrive, and the “net worth” the user sees is simply a fast read of an already-computed, already-cached number.
4.1 Position-Centric Data Model
The core unit of the system is a Position: (user_id, account_id, instrument_id, quantity, cost_basis, as_of_timestamp). A user’s net worth is nothing more than a well-defined fold over all of their positions plus liabilities:
net_worth = Σ(position.quantity × latest_price(position.instrument) × fx_rate(instrument.currency → reporting_currency)) − Σ(liability.balance × fx_rate)
Everything the system does — ingestion, streaming, caching — exists purely to keep the two inputs to this formula (quantities and prices) fresh, and to make evaluating the formula itself cheap.
4.2 Two Independent Triggers for Recomputation
A position’s contribution to net worth can change for exactly two reasons, and the system treats them as two independent event streams that both feed the same computation:
- Holdings change — the user bought/sold shares, deposited cash, a new account was linked, a sync discovered a changed balance. This is relatively low frequency per user (a handful of times a day at most) but must be handled with strong consistency: you cannot show the wrong number of shares.
- Price change — the market moved. This is extremely high frequency for popular instruments (many ticks per second) but the computation itself is stateless and can be handled with eventual consistency: a valuation that’s one second stale is perfectly fine.
4.3 The Instrument-to-Holder Index
The single most important internal data structure in the Valuation Engine is a reverse index: instrument_id → set of position_ids that hold it. Without this index, a single price tick for a popular stock would require a full table scan to find everyone affected. With it, a price tick becomes an O(k) operation where k is the number of positions holding that instrument. This index is kept in memory (partitioned across Valuation Engine instances by instrument, so each instance only needs to hold the index for the instruments it owns) and is rebuilt incrementally as holdings events arrive.
4.4 Debouncing and Significance Thresholds
Because popular instruments can tick many times per second, the Aggregation Service does not push every single recomputation to the client. It applies two techniques together: a time-based debounce window (e.g., collapse all changes for a user into at most one push per one to two seconds), and a significance threshold (only push if the net worth changed by more than a configurable amount, e.g., 0.01% or a minimum currency amount) so that a user watching a large, mostly-cash portfolio isn’t flooded with meaningless micro-updates. Both are tunable per client (a trading-focused user might want tighter, more frequent updates than a long-term investor).
“How would you avoid recomputing the same user’s net worth 500 times a second if they hold five popular stocks?” Debounce at the Aggregation Service using a per-user coalescing window (e.g., a keyed timer per user_id that only fires the rollup once every N milliseconds, always using the latest state at fire time), not at the Valuation Engine — the Valuation Engine should still process every tick to keep the cached position-level value fresh, but only the outward-facing rollup needs to be throttled.
4.5 Concurrency, Data Structures, and the CAP Theorem in Practice
It is worth being explicit about which parts of this system chose availability over consistency, and which chose the opposite, because the answer is different at different layers — this system does not make one single CAP-theorem decision, it makes several, deliberately, at different boundaries.
4.5.1 Data Structures on the Hot Path
The instrument-to-holder reverse index described earlier is typically implemented as a concurrent hash map keyed by instrument symbol, where each value is a compact set (or sorted array, for cache-friendliness) of position identifiers. Two properties matter here: lookups must be O(1) average case since they happen on every single tick, and updates to the index (when a user’s holdings change) must not block ongoing reads, since a stop-the-world lock on the index would introduce latency spikes across every instrument, not just the one being updated. This is why production implementations favor lock-free or fine-grained-locking concurrent structures, or partition the index further so that updates to one instrument’s holder set never contend with lookups for a different instrument.
A second important structure is the per-user debounce timer table inside the Aggregation Service — effectively a concurrent priority queue or hashed-wheel timer keyed by user_id, used to implement the coalescing window efficiently at scale (millions of independent timers firing at different times) without the overhead of millions of individually scheduled OS-level timers.
4.5.2 Concurrency Model
The Valuation Engine is typically built as a set of single-threaded (or single-writer) partitions, one per instrument-shard, each processing its events strictly in order. This sidesteps a large class of concurrency bugs entirely: rather than using locks to protect shared mutable state across threads, each partition owns its slice of state exclusively, and parallelism comes from running many partitions concurrently rather than many threads contending over one. This “shared-nothing per partition” model is the same underlying idea that makes Kafka Streams and Flink’s keyed state model scale well, and it maps naturally onto the instrument-keyed partitioning already chosen for the price-event stream.
4.5.3 CAP Theorem Trade-offs by Component
| Component | Choice under partition | Reasoning |
|---|---|---|
| Holdings Ledger | Consistency over availability (CP) | An incorrect share count is unacceptable; better to briefly reject a write than accept an inconsistent one. |
| Price Cache / Valuation Cache | Availability over consistency (AP) | A slightly stale price is harmless; the system should always answer, even with a slightly old number. |
| Net Worth Read Store | Availability over consistency (AP) | Users across devices may briefly see numbers a few seconds apart; always returning a last-known value beats returning an error. |
| Realtime Push Gateway | Availability over consistency (AP) | Missing or delaying one push update is acceptable since the client can always fall back to a REST read. |
This split is intentional and is one of the more important interview-worthy insights in this design: a single system is allowed — and often required — to make different CAP trade-offs at different internal boundaries, as long as each boundary is honest about which guarantee it is providing to the layer above it.
4.5.4 Replication, Partitioning, and Consensus
The ledger database uses leader-based replication with synchronous acknowledgment from at least one follower before a write is confirmed, using a consensus protocol (such as Raft, or a managed equivalent) under the hood to guarantee that a leader failover does not silently lose an acknowledged write. The streaming backbone (Kafka) uses its own replication and in-sync-replica quorum mechanism to guarantee that a published price or holdings event is durable before downstream consumers see it, so that a broker failure mid-processing cannot silently drop an event that a consumer has already acted on.
Partitioning, as covered in section 7, is chosen per-component to match its dominant access pattern: instrument-keyed for the price/valuation hot path, and user-keyed for the ledger and the read-facing aggregation layer. This means a single logical “net worth” computation actually crosses a partitioning boundary — from instrument-partitioned valuation data to user-partitioned aggregation — and that crossing is exactly what the PositionValuationChanged event stream in Figure 2 exists to bridge.
4.5.5 Failure Recovery
Because the Valuation Engine’s in-memory instrument index is derived state, not source-of-truth state, recovery from a crash is simple in principle: on restart, a partition reloads its holder index from the last checkpoint (periodically snapshotted state) and then replays any log entries since that checkpoint from Kafka, guaranteeing it converges to the same state it had before the crash. The same checkpoint-and-replay pattern applies to the Aggregation Service’s debounce/rollup state. This is a direct, practical application of event sourcing: because every input is an immutable, ordered, replayable log, no component needs to treat its in-memory state as precious — it can always be rebuilt.
“If the Valuation Engine crashes and restarts, how do you avoid a burst of stale valuations being briefly shown to users?” Serve the last cached, timestamped value with a staleness flag during the replay window rather than blocking reads, and only replace it once the partition has fully caught up — this is the same graceful-degradation principle applied to a recovery scenario rather than an external outage.
4.6 A Worked Example: Walking Through One User’s Rollup
It helps to make the abstract formula concrete. Consider a user with a reporting currency of USD who holds: 50 shares of a stock currently priced at 200 USD in a brokerage account, 2 BTC currently priced at 60,000 USD in a linked crypto exchange account, 5,000 EUR in a European bank account, a manually entered home valued at 400,000 USD, and a mortgage liability of 250,000 USD.
The Valuation Engine independently maintains the market value of each priced position: the equity position is valued at 50 × 200 = 10,000 USD, updated every time that stock ticks; the crypto position is valued at 2 × 60,000 = 120,000 USD, updated every time that exchange’s feed ticks. Note that these two positions are, from the Valuation Engine’s point of view, completely independent — they live on different partitions (keyed by their respective instrument symbols), are updated by unrelated events, and have no awareness of each other or of the user they both belong to.
The Aggregation Service is the first point in the pipeline where these independent pieces are actually combined for this specific user. It converts the 5,000 EUR cash position using the live EUR→USD rate (say 1.08, giving 5,400 USD), takes the equity and crypto valuations as already computed, includes the manually entered home value unchanged (since it has no live feed and simply carries forward its last saved value), and sums the four asset figures: 10,000 + 120,000 + 5,400 + 400,000 = 535,400 USD in total assets. It then subtracts the 250,000 USD mortgage liability, producing a net worth of 285,400 USD.
This example also illustrates why per-position freshness matters more than a single global timestamp: if the crypto exchange’s feed is live but the user’s bank connection failed its last sync six hours ago, the correct behavior is to show 285,400 USD with a note that the cash figure is six hours stale, rather than either hiding the number entirely or presenting it as fully live when part of it isn’t.
4.7 Handling Corporate Actions and Instrument-Level Edge Cases
Public equities are not static instruments — they undergo stock splits, mergers, ticker changes, and dividend payments, all of which can silently corrupt a valuation if not modeled explicitly. A 2-for-1 stock split, for example, doubles a user’s quantity while halving the per-share price, and if the holdings-change event and the price-change event for that split are processed out of order, a user could briefly see their position value halved or doubled incorrectly. Production systems handle this with a dedicated corporate actions feed that publishes split/merger events ahead of the price change, and the Valuation Engine applies quantity adjustments atomically with the corresponding price adjustment, keyed to the same effective timestamp, so the two updates are always consumed together rather than independently racing each other. Dividends, similarly, are modeled as a cash-generating event on the linked account (increasing the cash position) rather than as any change to the equity position’s valuation itself.
Data Flow & Lifecycle
It helps to walk through the full lifecycle from three different starting points: linking a new account, a normal price tick, and a user opening the app cold.
5.1 Lifecycle: Linking a New Account
This is modeled as a saga because it spans multiple systems (the OAuth provider, the token vault, the sync worker, and eventually the valuation pipeline) and any step can fail independently. A failed sync doesn’t roll back the OAuth grant; instead the account sits in an “Error” state, visible to the user, with automated retries.
5.2 Lifecycle: A Routine Price Tick (Steady State)
Covered in detail in section 4.3 and Figure 2 — this is the highest-frequency path in the system and is optimized purely for throughput and low per-event cost. It never touches a relational database on the hot path; everything needed is either in the in-memory instrument index or in the Redis valuation cache.
5.3 Lifecycle: Cold Read (User Opens the App)
- Client authenticates and requests
GET /users/{id}/net-worth. - API Gateway routes to the Net Worth Read Service.
- Read Service checks the Net Worth Read Store (a fast key-value or in-memory store) for a precomputed value.
- If present and fresh (updated within an acceptable staleness window), return immediately with a
last_updated_attimestamp and a per-account freshness flag. - If stale beyond the threshold (e.g., an account’s last sync failed hours ago), return the last known value along with a “stale” indicator per affected account, and asynchronously trigger a re-sync — never block the user-facing read on a live upstream call.
- Client also opens a WebSocket connection to the Realtime Push Gateway to receive subsequent live updates.
5.4 Historical Snapshotting
Independent of the live push path, the Aggregation Service writes a snapshot of net worth to a time-series store on a schedule (e.g., every 15 minutes during market hours, plus at market open/close, plus after any significant holdings change). This is deliberately decoupled from the live cache — the live cache answers “what is it right now,” the time-series store answers “how has it moved over time,” and they have very different read/write patterns and retention needs.
Advantages, Disadvantages & Trade-offs
| Design Choice | Advantage | Trade-off |
|---|---|---|
| Event-driven, stream-first architecture | Scales to high tick volume; decouples ingestion from computation. | Higher operational complexity; requires careful handling of out-of-order and duplicate events. |
| Materialized view / precomputed net worth | Sub-100 ms reads regardless of upstream latency. | The number can be briefly stale; requires a clear staleness contract with the user. |
| Debounced push updates | Prevents overwhelming clients and the network layer. | Introduces a deliberate, tunable delay between “true” market move and what the user sees. |
| Third-party aggregator for account linking | Fast time-to-market; broad institution coverage. | Vendor dependency, added latency, and a data-sharing trust boundary. |
| Eventual consistency for valuation | High availability and throughput. | Two devices for the same user may briefly show slightly different numbers. |
| Strong consistency for the holdings ledger | Never misrepresents how many shares a user owns. | Writes to the ledger are more constrained and slower than the price path. |
The unifying trade-off across nearly every decision in this system is freshness versus stability. A system that reflects every tick instantly would be technically impressive but practically useless — it would be expensive, noisy, and would make a portfolio feel more like a stock ticker than a considered wealth picture. The debounce and threshold mechanisms exist specifically to make “real-time” mean “trustworthy and current,” not “maximally twitchy.”
Performance & Scalability
At the scale this system targets — tens of millions of users, market-hours traffic spiking into millions of price-driven recomputations per minute — every hot-path component must be designed to scale horizontally and to avoid hot spots.
7.1 Partitioning the Valuation Engine by Instrument
Because the reverse index (instrument → holders) is the core data structure, the natural partition key for the Valuation Engine is the instrument symbol, not the user. Kafka partitions on the price-events topic are keyed by symbol, so all ticks for AAPL always land on the same Valuation Engine instance, which can maintain that instrument’s holder index entirely in memory without needing cross-instance coordination.
“What happens when one instrument (say, a viral meme stock) is held by a disproportionate number of users, creating a hot partition?” This is a classic hot-key problem. Mitigations include: sub-partitioning a single hot symbol across multiple consumer instances using a secondary hash (e.g., symbol + shard-of-user-id), pre-aggregating and rate-limiting the fan-out for extremely popular instruments, and monitoring partition lag as a first-class SLO so hot partitions are caught before they cause delays.
7.2 Partitioning the Aggregation Service and Read Store by User
The Aggregation Service and the Net Worth Read Store, by contrast, are naturally partitioned by user_id using consistent hashing, since all rollup logic and reads are scoped to a single user at a time.
7.3 Caching Strategy
Two distinct caches serve two distinct purposes: a price cache (latest known price per instrument, extremely hot, read millions of times per minute, written on every tick) and a net-worth read cache (final computed value per user, read on every app open, written only when the debounce window fires). Both use a cache-aside pattern with a Redis cluster, but they have very different TTLs and eviction priorities — the price cache can tolerate being wrong for milliseconds; the net-worth cache should never silently expire and return nothing, since the fallback must always be “last known good value,” never “no value.”
7.4 Batching and Micro-Batching
Rather than emitting one PositionValuationChanged event per position per tick, the Valuation Engine batches changes over a small window (tens of milliseconds) before publishing, reducing the message volume the Aggregation Service has to process and dramatically cutting network and serialization overhead at peak load.
7.5 Backpressure and Load Shedding
During extreme market volatility (a crash or a sharp rally), tick volume for popular instruments can spike far above normal. The pipeline must apply backpressure gracefully: Kafka consumer lag is monitored, and if the Valuation Engine falls behind, it can degrade by widening its own internal batching window (trading a little more staleness for throughput) rather than falling over. Load shedding at the push-gateway layer — dropping to a lower-frequency update cadence for a subset of users under extreme load — is a deliberate, monitored safety valve rather than an accident.
7.6 Read Replicas and CQRS
The system deliberately separates its write model (the append-only holdings ledger and price event stream) from its read model (the precomputed net-worth cache and time-series store) — a textbook CQRS split. This means the write path can be optimized for correctness and durability, while the read path is optimized purely for speed, replicated broadly, and scaled independently.
High Availability & Reliability
8.1 Graceful Degradation Over Hard Failure
If the Market Data Ingestion Service loses its connection to a price provider, the system should never simply stop showing a net worth. Instead, every cached value carries a timestamp, and the client displays “as of [time]” with a subtle staleness indicator once data exceeds a threshold age. The same applies if a single linked institution’s sync is failing — that one account is flagged stale while the rest of the user’s net worth continues to update normally.
8.2 Multi-Provider Redundancy for Market Data
Relying on a single market data vendor is a single point of failure for the entire platform. Production designs typically maintain at least a secondary price feed provider that can be failed over to automatically if the primary’s latency or error rate crosses a threshold, with a circuit breaker mediating the switch.
8.3 Idempotency and Exactly-Once Effective Processing
Kafka’s delivery guarantees are at-least-once by default, meaning the same price tick or holdings-change event can be delivered more than once. Every downstream consumer is built to be idempotent — a PriceUpdated event is keyed by (instrument, timestamp), and reprocessing the same event simply overwrites the same value rather than double-counting it. The holdings ledger uses similar idempotency keys tied to the source institution’s transaction identifiers.
8.4 Disaster Recovery
The holdings ledger (the true source of “who owns what”) is the most critical piece of state in the system and is treated with database-grade durability: synchronous replication across availability zones, point-in-time recovery, and regular backup restoration drills. The valuation and net-worth caches, by contrast, are fully rebuildable from the ledger and the latest prices, so they need only best-effort durability — losing the cache is an inconvenience (a brief recomputation delay), not a data-loss event.
8.5 Consensus and Replication
The Positions and Accounts DB (the ledger) typically runs on a strongly consistent, leader-based replicated store, so that a write acknowledging “user now owns 12 shares of AAPL” is never lost or reverted. The read-optimized stores downstream can run with weaker consistency guarantees (eventual consistency across regional replicas) precisely because they are derived, not authoritative.
“If the ledger and the cache disagree, which one wins?” The ledger always wins — it is the source of truth. Any observed mismatch is treated as a cache/valuation bug to be repaired by recomputing from the ledger, never by “correcting” the ledger to match the cache.
Security
9.1 Credential and Token Handling
The platform should never store a user’s actual bank or brokerage login credentials. Account linking uses OAuth (or an aggregator’s tokenized link flow) so that the platform only ever holds a short-lived or revocable access token, not a password. These tokens are stored in a dedicated Token Vault — encrypted at rest with a key management service, accessible only to the Account Linking and Sync services, and never logged.
9.2 Encryption
All data in transit uses TLS, including internal service-to-service traffic. Sensitive financial data at rest — account numbers, balances, positions — is encrypted at the storage layer, with field-level encryption for the most sensitive identifiers (account numbers, tax IDs) so that even a database-level breach doesn’t expose them in plaintext.
9.3 Access Control
Internal services follow least-privilege role-based access control: the Valuation Engine, for example, needs read access to instrument holdings but has no business reading a user’s linked bank account numbers. Every internal API call carries a scoped service identity, and access to raw financial data is further gated by audit-logged, need-to-know internal tooling for support staff.
9.4 Compliance Posture
A platform handling linked financial accounts typically needs to operate under a SOC 2 Type II control framework at minimum, and depending on jurisdiction and whether it executes trades or merely aggregates data, may need to consider regulations around financial data aggregation, consumer data rights (allowing a user to fully revoke and delete linked account data), and data residency requirements.
9.5 Abuse and Fraud Considerations
Rate limiting and anomaly detection on the account-linking flow prevent credential-stuffing or token-theft attempts from being used to enumerate or scrape linked account data. Unusual patterns — a token suddenly being used from a new region, or a sudden spike in account-linking attempts from one client — are flagged for step-up authentication or automatic token revocation.
“How would you securely revoke access when a user unlinks an account?” Immediately delete or revoke the stored token at the institution/aggregator (not just mark it inactive locally), purge cached position data for that account after a grace period, and emit an audit event. The unlink action itself should be synchronous and confirmed to the user, even if downstream cleanup of derived data (like historical snapshots) happens asynchronously per data-retention policy.
9.6 Defense in Depth Across the Pipeline
Security in this system is not a single wall around the perimeter; it is layered at every boundary the data crosses. At the ingress boundary, every external call — from a client app or from an institution’s webhook — is authenticated, validated against a strict schema, and rate limited, so that a malformed or malicious payload from a compromised partner integration cannot propagate deeper into the pipeline. Internally, service-to-service traffic uses mutual TLS so that a compromised service cannot simply impersonate another by calling its internal endpoint; combined with the least-privilege access control described above, this limits the blast radius of any single compromised component. At the data boundary, the Token Vault, the Ledger, and the audit-logging subsystem are deliberately isolated into their own security domains with tighter access controls than the general application tier, since they are the components whose compromise would be most damaging.
9.7 Handling Third-Party Aggregator Risk
Relying on a financial data aggregator for account linking introduces a trust boundary the platform does not fully control. Mitigating this risk involves minimizing the data actually persisted from the aggregator (storing only what valuation and display require, not a full copy of every transaction line item unless it’s genuinely needed), contractually and technically verifying the aggregator’s own security posture, and designing the Account Linking Service so that a full aggregator outage degrades gracefully — existing linked accounts continue to show their last-synced data — rather than becoming a hard platform-wide failure.
Balances, account numbers, and tax IDs must never appear in general application logs. Route them only to dedicated, access-controlled audit trails with retention policies and reviewer sign-off.
Monitoring, Logging & Metrics
10.1 The Metrics That Actually Matter Here
End-to-end freshness lag
The time between a real-world price change and the moment it’s reflected in a user’s pushed net-worth update. The single most important SLO for a “real-time” system — tracked as a percentile distribution (p50/p95/p99), not an average.
Sync success rate per institution
Tracked per-institution, since a single flaky partner can silently degrade data quality for a subset of users without affecting overall system health metrics.
Kafka consumer lag
Per topic/partition — an early warning signal for backpressure before it becomes visible to users.
Net-worth cache hit rate
A sudden drop usually signals a cold-start problem after a deploy or a cache eviction storm.
WebSocket health
Active connections, reconnect rate, and message delivery failures on the push gateway.
10.2 Distributed Tracing
Every price tick and every valuation update carries a correlation ID that threads through the entire pipeline — from the Market Data Ingestion Service, through the Valuation Engine, through the Aggregation Service, to the WebSocket push. This lets an engineer trace exactly why a specific user’s update was delayed: was it consumer lag, a debounce window, or a downstream push failure.
10.3 Logging Discipline
Structured logs are used throughout, and — critically — financial values and account identifiers are never logged in plaintext in general-purpose application logs; they are only accessible through dedicated, access-controlled audit trails, separate from the operational logging pipeline used for debugging.
10.4 Alerting
Alerts are tiered: a spike in freshness lag across the whole platform pages on-call immediately; a single institution’s sync failure rate crossing a threshold generates a lower-urgency ticket, since a subset of users being briefly stale is degraded service, not an outage.
Deployment & Cloud
The stateless computation services — Valuation Engine, Aggregation Service, API Gateway, Realtime Push Gateway — are natural fits for containerized deployment on Kubernetes, scaled horizontally with autoscaling policies driven by custom metrics (Kafka consumer lag and active WebSocket connections, not just CPU) since those better reflect true load for a stream-processing system.
Sync Workers, which spend most of their time waiting on external institution APIs, are well suited to a worker-pool model with independent autoscaling, since their bottleneck is I/O concurrency rather than CPU.
Multi-region deployment is important for two reasons: latency (serving users from the region closest to them) and resilience (a full regional outage should not take down the platform). The holdings ledger typically has a primary region with cross-region replication for disaster recovery, while read-optimized caches and the push gateway are deployed active-active across regions, routed by geo-aware load balancing.
A blue-green or canary rollout strategy is particularly important for the Valuation Engine, since a bug in valuation logic directly produces an incorrect dollar figure shown to users — this is exactly the kind of change that benefits from a slow, metric-gated rollout rather than a full deploy.
11.1 Deployment Topology in Practice
A typical production topology separates workloads into distinct node pools tuned to their resource profile: the Valuation Engine and stream-processing tier benefit from memory-optimized nodes, since they hold large in-memory partitioned state; Sync Workers benefit from nodes tuned for high network concurrency rather than raw compute, since they spend most of their time waiting on slow external APIs; and the Realtime Push Gateway benefits from nodes tuned for a high number of concurrent open connections rather than CPU-bound work.
Configuration and secrets (API keys for market data vendors, aggregator credentials, database connection details) are managed through a centralized secrets manager rather than baked into container images or environment variables checked into source control, and infrastructure itself — the Kafka cluster sizing, the Kubernetes node pools, the database replica topology — is defined declaratively as infrastructure-as-code, so that a disaster-recovery region can be stood up predictably rather than manually.
11.2 Cost Optimization
Market data licensing and aggregator API calls are typically the largest variable cost in a system like this, which is exactly why the architecture is built around minimizing redundant external calls — caching prices in a shared layer rather than each service fetching independently, and syncing holdings on an adaptive schedule (more frequently for active traders, less frequently for dormant accounts) rather than a fixed interval for every account regardless of activity. On the compute side, autoscaling policies tied to real load signals (Kafka lag, active connections) rather than fixed always-on capacity keep infrastructure cost proportional to actual usage, particularly important given how much traffic concentrates into market-hours windows.
Databases, Caching & Load Balancing
Ledger store
The Positions and Accounts DB (the ledger) benefits from strong consistency and relational integrity (an account belongs to a user, a position belongs to an account, quantities must never go negative unexpectedly) — a good fit for a distributed relational database with strong transactional guarantees, partitioned by user_id or account_id.
Streaming backbone
Kafka (or an equivalent distributed log) is the natural backbone for both the price-event stream and the holdings-change stream, because it provides ordered, replayable, partitioned delivery — replayability specifically matters here, since it means the Valuation Engine’s in-memory state can always be rebuilt from scratch by replaying the log if an instance restarts.
Cache layer
Redis (or a similar in-memory store) backs both the price cache and the net-worth read cache, chosen for its low-latency reads, native TTL support, and pub/sub capability, which the Realtime Push Gateway can use directly to fan out updates to connected clients.
Time-series store
Historical net-worth snapshots are high-write, append-mostly, time-ordered data queried by range — the textbook use case for a time-series-optimized database, which offers efficient compression and range-query performance far better than a general-purpose relational table would at this volume.
12.5 Load Balancing
Standard Layer 7 load balancing in front of the stateless API Gateway and REST services; for the Realtime Push Gateway, load balancing must be connection-aware — since WebSocket connections are long-lived and stateful, the load balancer needs consistent routing (or the gateway needs a shared pub/sub backbone via Redis) so that a net-worth update generated anywhere in the system can find its way to the specific gateway instance holding that user’s live connection.
“How does the Aggregation Service know which gateway instance holds a given user’s WebSocket connection?” It doesn’t need to — it publishes the update to a Redis pub/sub channel (or Kafka topic) keyed by user_id, and every gateway instance subscribes to the channels for the users currently connected to it, forwarding matching messages down the socket. This decouples “who computed the update” from “who is holding the connection.”
APIs & Microservices
The system exposes a small, clean external API surface even though a great deal of internal complexity sits behind it:
Account Linking API
Initiate a link, list linked accounts, unlink an account.
Net Worth Read API
Get current net worth, get historical net worth over a time range, get a breakdown by asset class or account.
Manual Asset/Liability API
Create, update, or delete a manually tracked item (real estate, a personal loan).
Realtime Subscription API
A WebSocket or SSE endpoint clients connect to for live push updates.
Internally, the boundaries between microservices are drawn along the lines of different rates of change and different consistency needs — Account Linking and the Ledger need strong consistency and change relatively rarely; Market Data Ingestion and the Valuation Engine are extremely high-throughput and can tolerate eventual consistency; the Aggregation Service sits in between, consuming from both. This is a deliberate application of the single-responsibility principle at the service level: each service owns exactly one axis of the net-worth formula.
Communication between services is a mix of synchronous REST/gRPC for request-response needs (like the Account Linking flow) and asynchronous event streaming for anything on the valuation hot path — a synchronous call from the Valuation Engine to the Aggregation Service for every tick would introduce coupling and latency that the architecture is specifically designed to avoid.
Design Patterns & Anti-patterns
Patterns Used
- CQRS (Command Query Responsibility Segregation) — the write-side ledger and the read-side net-worth cache are structurally and technologically separate, each optimized for its own access pattern.
- Event Sourcing (partial) — the holdings-change stream and price-event stream act as an append-only log of everything that happened, from which current state (and historical state at any point in time) can be derived.
- Materialized View — the net-worth cache is a continuously updated, precomputed answer to an expensive query, rather than that query being run live on every read.
- Saga — the multi-step account-linking flow, with compensating retries rather than distributed transactions.
- Circuit Breaker — wraps every call out to an external institution or market data provider, preventing one slow or failing partner from cascading into the rest of the system.
- Strategy Pattern — valuation logic is pluggable per asset class (an equity valuator, a crypto valuator, a real-estate AVM valuator, a manual valuator), all implementing a common “value this position” interface, so new asset classes can be added without touching core aggregation logic.
- Publish-Subscribe / Fan-out-Fan-in — a single price tick fans out to every affected position; individual valuation changes fan back in to a single per-user rollup.
- Cache-Aside — both the price cache and net-worth cache are read-through/write-behind caches sitting in front of authoritative computed state.
Anti-Patterns to Avoid
- Synchronous fan-out to institutions on every read. Calling out to eight external APIs every time a user opens the app is the single biggest mistake possible in this design — it is slow, expensive, unreliable, and will get the platform rate-limited or blocked by partners.
- One monolithic “calculate everything” job. A nightly batch job that recomputes every user’s net worth from scratch defeats the entire purpose of “real-time” and doesn’t scale as the user base grows — the incremental, event-driven approach is what makes both real-time freshness and cost efficiency possible simultaneously.
- Tightly coupling valuation logic to specific asset classes inside the Aggregation Service. Hardcoding “if asset_type == crypto then…” branches throughout the rollup logic makes it brittle and hard to extend; the Strategy pattern above exists specifically to avoid this.
- Treating price staleness as a binary (fresh/broken) rather than a spectrum. A UI that either shows a live number or a hard error is worse than one that clearly communicates “as of 4 minutes ago” — users trust transparency about staleness far more than a system that goes silent.
- No idempotency on ledger writes. Re-processing a duplicated “deposit” event from a bank webhook without an idempotency key can silently double-count a user’s cash balance — a category of bug that is especially dangerous in a financial system.
Best Practices & Common Mistakes
15.1 Best Practices
Attach source + timestamp
Always attach a timestamp and source to every price and every balance, and surface that provenance in the API response — never present a single opaque number without a freshness/trust signal.
Pure valuation formula
Design the valuation formula to be pure and side-effect-free (quantity × price × fx-rate), so it can be tested exhaustively and re-run deterministically for auditing or dispute resolution.
Idempotent from day one
Make every event in the pipeline idempotent from day one; retrofitting idempotency after a production double-counting incident is far more painful than designing for it upfront.
Staleness as first-class
Treat “staleness” as a first-class field in every API response, not an afterthought — clients should always be able to tell the user how current a number is.
Keep the ledger boring
Keep the holdings ledger boring, relational, and strongly consistent. Resist the temptation to make it “fast and eventually consistent” — this is the one place where correctness must trump speed.
Build a backfill tool early
Build a replay/backfill tool for the Valuation Engine early — the ability to say “recompute this user’s net worth history from the ledger and historical prices” is invaluable for debugging disputes and for onboarding new asset classes.
15.2 Common Mistakes
- Underestimating how differently various asset classes need to be valued, and building a valuation engine that only really works well for public equities, then bolting on special cases for everything else.
- Ignoring currency conversion until late in the design, then discovering that “net worth” silently assumed a single currency the whole time.
- Pushing every price tick to the client without debouncing, leading to battery drain, UI jank, and a system that feels twitchy rather than trustworthy.
- Not planning for partial failure — designing the happy path where every linked account syncs successfully, and only later realizing that partial staleness (some accounts fresh, some stale) needs to be a designed-for state, not an edge case.
Real-World Examples
Personal financial dashboards
Personal financial dashboards that aggregate brokerage, bank, and retirement accounts (in the spirit of products like Empower/Personal Capital) popularized the “linked accounts + net worth trend line” experience, generally refreshing holdings on a periodic sync rather than sub-second live pricing.
Multi-asset portfolio tracking
Portfolio-tracking apps focused on investors (in the spirit of products like Kubera, which explicitly covers crypto wallets, real estate, and collectibles alongside brokerage accounts) push further into true multi-asset-class aggregation, including manually tracked illiquid assets.
Crypto-native trackers
Crypto-native portfolio trackers push hardest on live pricing, since crypto markets trade continuously and users expect tick-level freshness.
In-house brokerage & bank apps
Large brokerages and banks that show “total account value” inside their own apps solve a narrower version of this problem — usually a single institution, a single ledger, and native access to their own live trading data — which sidesteps the account-aggregation and multi-institution-reliability challenges that dominate a cross-institution net-worth platform like the one designed here.
None of these products’ internal architectures are publicly documented in detail, and the design in this tutorial is a generalized, first-principles system — not a description of any specific company’s actual implementation — but the shape of the problem (federated data sources, mixed valuation models, freshness-versus-cost trade-offs) is consistent across the category.
FAQ, Summary & Key Takeaways
Why not just recompute net worth from scratch on every request?
Because the inputs live in dozens of external systems with their own latency and rate limits — a synchronous, on-demand computation would be slow, expensive, and fragile. Precomputing continuously in the background and serving from cache is what makes both speed and reliability possible.
How “real-time” is real-time here, really?
In practice, “real-time” means updates delivered within one to a few seconds of a meaningful change, not microsecond-level trading latency. The debounce and significance-threshold mechanisms are a deliberate design choice, not a limitation — true tick-by-tick display would be both wasteful and unpleasant to look at for a net-worth product.
What happens if two different asset classes disagree on how “current” they can be?
Each asset class carries its own freshness expectation and staleness threshold — a stock price stale by five seconds is a problem; a real estate estimate that’s a month old is completely normal. The system tracks per-position freshness rather than assuming one global freshness SLA for the whole portfolio.
How would this system handle a user with no linked accounts, only manual entries?
The same pipeline applies — a manual asset or liability is just a position/liability record with a “manual” valuation strategy instead of a live price feed, updated only when the user edits it, and it still flows through the same Aggregation Service and rollup logic as everything else.
How would you extend this design to support multiple currencies as a first-class concept, not just a conversion at the end?
Store every position’s native currency alongside its quantity and instrument, and treat the reporting currency as a per-request (or per-user-preference) parameter rather than a stored fact. The FX Service supplies live rates the same way the Market Data Service supplies prices, and currency conversion is applied as the very last step of the rollup formula, not baked into any stored value — this way, changing a user’s display currency never requires touching the underlying ledger, only re-running the read-side computation with a different rate.
What’s the difference between “position value” staleness and “net worth” staleness?
A single position can be stale because its price feed or its account sync is behind, while the overall net worth figure is a weighted blend of many positions with different freshness. The API should expose both: a per-position as_of timestamp for transparency, and an overall staleness indicator on the total that reflects the least fresh material contributor — a small stale cash position shouldn’t flag the whole portfolio as stale, but a large stale brokerage account should.
How do you test a system like this without connecting to real markets or real bank accounts?
Every external boundary — market data feeds, institution sync APIs, FX rates — is abstracted behind an interface with a deterministic test double that can replay recorded tick sequences and simulated account states. Because the valuation formula itself is pure and stateless, it can be property-tested exhaustively (e.g., “net worth must equal the sum of account totals must equal the sum of position values, for any random combination of holdings and prices”), catching rounding and aggregation bugs long before they reach production traffic.
How would you support “what if” scenarios, like showing a user their net worth if a stock they own moved by 10%?
Because the valuation formula is a pure function of (quantity, price, fx-rate), a what-if feature can reuse the exact same Strategy-pattern valuators used in production, simply substituting a hypothetical price for the live one, entirely on the read path — no changes to the ledger, the streaming pipeline, or any stored state are needed, which is a good sign that the core computation was designed with the right separation of concerns.
This system treats “net worth” not as a query to be answered on demand, but as a continuously maintained materialized view — assembled from two independent, differently-paced event streams (holdings changes and market prices), computed incrementally through an event-driven pipeline, cached aggressively for fast reads, and delivered live through a debounced, threshold-gated push layer. The hardest problems in the design are not computational (the math is simple multiplication and addition); they are about correctness under partial failure, freshness-versus-cost trade-offs, and scaling a fan-out from a small number of price ticks to millions of affected positions without falling over.
Key Takeaways
- Net worth = Σ(position value in reporting currency) − Σ(liabilities), computed as a materialized view, never live-fetched on read.
- Holdings changes and price changes are two independent event streams with different frequencies and different consistency requirements — model them separately.
- An in-memory instrument-to-holder reverse index is what makes a single price tick cheaply fan out to every affected position.
- Debouncing and significance thresholds are essential for both cost control and user experience — “real-time” does not mean “every single tick.”
- The holdings ledger needs strong consistency; the valuation and rollup layers can and should be eventually consistent for scalability.
- Graceful degradation (stale-but-labeled data) beats hard failure every time in a financial product.
- CQRS, Event Sourcing, Materialized View, Saga, Circuit Breaker, and Strategy are the load-bearing design patterns of this system.
Freshness versus stability is the unifying trade-off. Debounce and threshold mechanisms exist to make “real-time” mean “trustworthy and current,” not “maximally twitchy.”
Materialized view, not query
Precompute continuously. The user-facing read is always O(1) from cache.
Two streams, one formula
Holdings and prices flow separately but converge at the Aggregation Service.
Reverse index is king
Instrument → positions makes fan-out O(k), not O(users).
CAP per boundary
CP ledger, AP caches. One system, many honest guarantees.
Stale-with-label > error
Show the last good number and mark it. Never go silent on money.
Idempotency day one
Retrofitting after a double-count incident is expensive and painful.