Decision log
The Portfolio App has more than fifty written architecture decisions. These are six I would point to, in the order they were made.
Store portfolio state instead of replaying the ledger
Every holdings request replayed a portfolio's whole transaction history, and above 200 transactions the dashboard's value chart was simply wrong, because it only ever saw one page of them.
I now store positions and a daily value series, written by the same replay that checks each new transaction and rebuilt when an older one changes.
At 5,000 transactions a holdings read went from 51.8 ms to 0.27 ms.
Compute risk inside the request, with no queue
The plan said heavy risk jobs would go through a Celery queue. Measured on a portfolio larger than any the app can produce today, 50 holdings over 1,299 trading days with a 10,000-path Monte Carlo, the whole report took 62.4 ms.
A worker polling Upstash Redis would also have spent about 2.6 million commands a month doing nothing, five times the plan's budget.
So risk is computed inside the request and cached in Redis, and a queue can come back when there is work slow enough to need one.
The capital gains engine stores nothing
Under the 30-day rule, a purchase made next week can change how a sale made today is taxed, so any stored tax result can be wrong the moment it is written.
The tax service therefore keeps no database and no cache: every request replays the full history through one pure function, and even today's date is passed in rather than read from a clock.
Replaying a retail-sized ledger in Rust takes microseconds, and the same purity made the engine easy to pin against HMRC's worked examples and to run in a browser.
No connection pre-ping
Every Python service checked that its database connection was alive before each query. Inside the staging cluster that took a checkout and query from 23.3 ms to 49.1 ms, and a portfolio summary paid it three times.
The check guarded against Neon suspending idle compute, which routing through Neon's always-on connection pooler had already fixed.
I removed it and kept connections recycling every five minutes; the cost is that a connection lost between requests now fails one query instead of quietly reconnecting.
A stock split is a ledger row
A hand-entered portfolio could not record a stock split, and the broker import modelled one as free shares arriving.
That put the new shares in the wrong position and let the tax engine match them to the wrong sale: applying HMRC's rules to a simple case, the difference was a £60 gain against £180.
A split is now its own transaction type in the ledger, so the holding changes on the right day and the tax engine knows the share count changed rather than seeing new shares bought.
Measure a return against the capital it used
The P&L screen divided a period's gain by the holdings at the start plus any deposits. Virtual portfolios record no deposits, so a portfolio that started with 10,000 and then bought 90,000 more was measured against 10,000, and a 1% month showed as 10%.
The base is now the opening holdings plus whichever is larger: net deposits in the period, or the extra capital its purchases tied up.
The chart computes the same base at every point, so its last value always matches the headline figure.