Low Level Design: Investment Portfolio Tracker

Problem Statement

Design an investment portfolio tracker that stores positions, pulls real-time price feeds, calculates gain/loss, alerts on rebalancing drift, and tracks tax lots for cost-basis reporting.

Core Requirements

  • Position storage per user and account
  • Real-time and end-of-day price feed ingestion
  • Unrealized and realized gain/loss calculation
  • Portfolio rebalancing alerts when allocation drifts from target
  • Tax lot accounting (FIFO, LIFO, specific identification)

Data Model

users
  id            BIGINT PK
  email         VARCHAR(255) UNIQUE NOT NULL

accounts
  id            BIGINT PK
  user_id       BIGINT FK -> users.id
  name          VARCHAR(100)
  account_type  ENUM('taxable','ira','roth','401k')
  currency      VARCHAR(3) DEFAULT 'USD'

positions
  id            BIGINT PK
  account_id    BIGINT FK -> accounts.id
  symbol        VARCHAR(20) NOT NULL
  quantity      DECIMAL(18,6) NOT NULL
  updated_at    TIMESTAMP
  UNIQUE (account_id, symbol)

tax_lots
  id            BIGINT PK
  position_id   BIGINT FK -> positions.id
  quantity      DECIMAL(18,6) NOT NULL
  cost_basis    DECIMAL(18,6) NOT NULL     -- per share
  acquired_at   TIMESTAMP
  lot_method    ENUM('fifo','lifo','specific')

transactions
  id            BIGINT PK
  account_id    BIGINT FK -> accounts.id
  symbol        VARCHAR(20) NOT NULL
  type          ENUM('buy','sell','dividend','split','transfer')
  quantity      DECIMAL(18,6)
  price         DECIMAL(18,6)
  fee           DECIMAL(10,4) DEFAULT 0
  transacted_at TIMESTAMP

prices
  symbol        VARCHAR(20) NOT NULL
  price         DECIMAL(18,6) NOT NULL
  source        VARCHAR(50)
  recorded_at   TIMESTAMP
  PRIMARY KEY (symbol, recorded_at)

targets
  id            BIGINT PK
  account_id    BIGINT FK -> accounts.id
  symbol        VARCHAR(20) NOT NULL
  target_pct    DECIMAL(7,4) NOT NULL
  tolerance_pct DECIMAL(7,4) DEFAULT 5.0

API Design

GET    /accounts/{accountId}/positions        Current positions with live value
GET    /accounts/{accountId}/performance      Total gain/loss summary
GET    /accounts/{accountId}/tax-lots         All open lots with unrealized gain
POST   /accounts/{accountId}/transactions     Record a buy or sell
GET    /accounts/{accountId}/rebalance        Drift from target allocations
PUT    /accounts/{accountId}/targets          Set or update target allocations
GET    /prices/{symbol}                       Latest and historical prices

Position and Value Calculation

-- Current market value per position
SELECT
  p.symbol,
  p.quantity,
  pr.price AS current_price,
  p.quantity * pr.price AS market_value
FROM positions p
JOIN LATERAL (
  SELECT price FROM prices
  WHERE symbol = p.symbol
  ORDER BY recorded_at DESC LIMIT 1
) pr ON true
WHERE p.account_id = :account_id;

-- Unrealized gain/loss per tax lot
SELECT
  tl.quantity,
  tl.cost_basis,
  pr.price AS current_price,
  (pr.price - tl.cost_basis) * tl.quantity AS unrealized_gain
FROM tax_lots tl
JOIN positions p ON tl.position_id = p.id
JOIN LATERAL (...) pr ON true
WHERE p.account_id = :account_id;

Price Feed Ingestion

A price ingestor service subscribes to market data providers (e.g., Polygon, IEX Cloud) via WebSocket for real-time quotes during market hours and via REST for end-of-day closes. Prices are written to the prices table and also cached in Redis with a short TTL for fast reads during portfolio valuation.

Redis key: price:{symbol}
Value:     {"price": 182.45, "ts": 1713360000}
TTL:       15 seconds during market hours, 3600 seconds after close

Tax Lot Accounting

Each buy transaction creates a new tax lot. When shares are sold the service selects which lots to close based on the user's chosen method:

  • FIFO: consume oldest lots first
  • LIFO: consume newest lots first
  • Specific identification: user nominates lot IDs at sale time

Realized gain equals (sale price minus cost basis) times quantity sold. Lots are partially consumed when a sale quantity falls within a single lot.

-- FIFO lot selection for a sell of :sell_qty shares
SELECT id, quantity, cost_basis
FROM tax_lots
WHERE position_id = :position_id
ORDER BY acquired_at ASC
-- application iterates rows, deducting sell_qty until exhausted

Rebalancing Alerts

A scheduled job computes current allocation percentages and compares them against target percentages. When any position drifts beyond tolerance_pct, an alert is generated.

total_value = SUM(position.quantity * current_price) for all positions

for each position:
  current_pct = (position.quantity * current_price) / total_value * 100
  drift = ABS(current_pct - target.target_pct)
  if drift > target.tolerance_pct:
    create alert(account_id, symbol, current_pct, target.target_pct, drift)
    emit BUY or SELL suggestion to reach target_pct

Performance Calculation

Time-weighted return (TWR) is the standard for portfolio performance because it neutralizes the impact of cash flows. The service snapshots portfolio value daily after close and uses the chain-linking formula across sub-periods. For simpler needs, a simple return on cost basis is also exposed.

Scaling Considerations

  • Pre-compute and cache portfolio snapshots nightly for historical performance charts
  • Price reads go through Redis; prices table is used for history and audit only
  • Partition prices table by month — it grows unboundedly
  • Fan-out rebalancing checks as background jobs keyed by account_id to avoid one slow account blocking others
  • Use event sourcing for transactions so the full lot history can always be reconstructed

Interview Tips

  • Lead with the tax lot model — most candidates miss it and it immediately differentiates the design
  • Clarify real-time vs. end-of-day requirements early; real-time needs WebSocket feeds and Redis, EOD is much simpler
  • Explain why TWR is preferred over simple return for performance measurement — it shows domain knowledge
  • Discuss account types (taxable vs. IRA) as they affect which tax lot methods are relevant
What is an investment portfolio tracker in system design?

An investment portfolio tracker is a system that aggregates a user’s holdings across one or more brokerage accounts, computes current market value using live or end-of-day price feeds, and presents performance metrics such as total return, asset allocation, and gain/loss per position. Core components include a transaction ingestion pipeline (manual entry or brokerage API), a lot-tracking ledger that records every buy and sell with its cost basis, a price service that fetches and caches quotes, and a computation layer that applies accounting methods (FIFO, LIFO, average cost) to produce tax-lot P&L.

How does FIFO vs LIFO lot selection affect tax calculations?

When you sell shares, the tax owed depends on which specific lots (purchase events) are considered sold. Under FIFO (First In, First Out) the oldest lots are sold first, which typically realizes long-term gains if shares have been held over a year — often taxed at a lower rate. Under LIFO (Last In, First Out) the most recently purchased lots are sold first, which can minimize gains in a rising market by matching sales against higher-cost recent purchases. The portfolio tracker must maintain a chronological lot ledger per symbol and, at sale time, iterate through lots in the configured order, consuming quantity and recording realized gain or loss for each lot consumed.

How do you calculate time-weighted return (TWR) for a portfolio?

Time-weighted return eliminates the distorting effect of external cash flows (deposits and withdrawals) so that portfolio performance reflects only investment decisions. The calculation breaks the total period into sub-periods, each ending on a cash-flow date. For each sub-period compute the holding-period return: (ending value – beginning value) / beginning value, where beginning value already includes the prior period’s ending balance. Chain-link the sub-period returns by multiplying (1 + r) factors together, then subtract 1 for the total TWR. In a system, this requires storing end-of-day portfolio snapshots and a log of all cash flows with timestamps so sub-periods can be reconstructed on demand.

How do you handle corporate actions like splits and dividends in a portfolio tracker?

Corporate actions require adjusting historical lots so that cost basis and share counts remain consistent. For a stock split (e.g., 2-for-1) every existing lot’s share count is multiplied by the split ratio and its per-share cost basis is divided by the same ratio, keeping total cost basis unchanged. For a cash dividend the tracker records a cash inflow transaction on the ex-dividend date; for a stock dividend or stock spin-off, new lots are created with an allocated cost basis derived from the fair market values of old and new securities on the distribution date. A corporate actions feed (from a data vendor or manual entry) triggers these adjustments, and the tracker must reprocess any performance calculations that used pre-adjustment data.

See also: Coinbase Interview Guide

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

See also: Databricks Interview Guide 2026: Spark Internals, Delta Lake, and Lakehouse Architecture

Scroll to Top