<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>lollilog</title><description>Leading teams, building things: notes on leadership and on the software I build and run at home.</description><link>https://lollilog.com</link><item><title>Not the expert in the room</title><link>https://lollilog.com/blog/in-praise-of-generalists</link><guid isPermaLink="true">https://lollilog.com/blog/in-praise-of-generalists</guid><description>I work with people who are the best at what they do, and I&apos;m not. What I&apos;m learning about being a generalist among experts: the doubts, and the ways it helps.</description><pubDate>Wed, 14 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most days I work with people who are much better than me at what they do.
Some have PhDs in the exact field we&apos;re discussing. Some have spent ten
years on one system and know every corner of it. When they talk about their
area, I can follow, mostly, but I couldn&apos;t do what they do.&lt;/p&gt;
&lt;p&gt;I&apos;m not the best at anything. I&apos;m okay at quite a lot of things: a bit of
code, a bit of infrastructure, a bit of product, a bit of people. For a long
time I thought that was something to hide, or a gap I&apos;d eventually close by
picking one thing and going deep.&lt;/p&gt;
&lt;p&gt;I don&apos;t have this figured out. But I&apos;ve started to see it differently, and
I&apos;m writing down where I&apos;ve got to, in case it helps someone else who feels
the same in a room full of experts.&lt;/p&gt;
&lt;h2&gt;What it feels like&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./window.svg&quot; alt=&quot;An office building at night: rows of dark windows and one warm, lit window with a person working alone.&quot;&gt;&lt;/p&gt;
&lt;p&gt;There&apos;s a particular kind of quiet I&apos;ve learned to recognise. A discussion
goes deep into something I only half understand. Everyone around the table
seems to be speaking fluently, and I&apos;m translating in my head, one sentence
behind. I nod. I write down a couple of words to look up later. I wait for
the moment where I can add something useful, and sometimes it doesn&apos;t come.&lt;/p&gt;
&lt;p&gt;Afterwards there&apos;s a small feeling of being alone. Not left out: people are
kind, and nobody makes me feel small. It&apos;s more that I can see the outline of
what they see, but not all of it, and I know I won&apos;t catch up on that topic
the way they have, because tomorrow I&apos;ll be in a different room on a
different topic.&lt;/p&gt;
&lt;p&gt;And then there&apos;s the familiar voice: &lt;em&gt;one day they&apos;ll notice&lt;/em&gt;. That I don&apos;t
really know the details. That I&apos;m in the room because I&apos;m good at sounding
like I belong. The name for it, impostor syndrome, makes it sound like a
condition you get over. In my experience it doesn&apos;t go away with seniority.
If anything it gets louder, because the more senior you are, the more topics
you&apos;re expected to have an opinion on, and the more experts you sit with.&lt;/p&gt;
&lt;p&gt;I&apos;m not writing this to complain. I&apos;m writing it because for years I assumed
I was the only one who felt like this, and I&apos;ve since found out I wasn&apos;t.&lt;/p&gt;
&lt;h2&gt;What I bring, if I&apos;m not the expert&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./keys.svg&quot; alt=&quot;A big keyring holding many different keys: none of them a master key, together they open a lot of doors.&quot;&gt;&lt;/p&gt;
&lt;p&gt;What helped me most was noticing what actually happens when things go well.
It&apos;s rarely me knowing the answer. It&apos;s usually one of these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Translating.&lt;/strong&gt; Explaining what the specialists are worried about to the
people who need to decide, and the other way round, in words each side
cares about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connecting.&lt;/strong&gt; Noticing that two teams are solving the same problem from
different ends, or that an idea from one area fits another.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seeing the whole.&lt;/strong&gt; The slow page that&apos;s really a data problem. The
&quot;process issue&quot; that&apos;s really two teams optimising for different things.
When you know a little about every layer, you sometimes see the joint
that&apos;s failing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Asking the naive question.&lt;/strong&gt; The one everyone else assumed was already
answered.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these need me to be the deepest person in the room. They need me to
be roughly fluent in many rooms. My side projects are the clearest example I
have: to build my finance tracker I wrote the backend and the frontend, set
up the release pipeline, the monitoring and the small home cluster it runs
on. No single part of it would impress a specialist. But I could build the
whole thing, and understand how each part affects the others.&lt;/p&gt;
&lt;p&gt;A book that helped me make peace with this was &lt;em&gt;Range&lt;/em&gt; by David Epstein. One
line in it describes exactly the tension I feel:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The challenge we all face is how to maintain the benefits of breadth,
diverse experience, interdisciplinary thinking, and delayed concentration in
a world that increasingly incentivizes, even demands, hyperspecialization.&quot;&lt;/p&gt;
&lt;p&gt;— David Epstein, &lt;em&gt;Range&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That&apos;s the job, as I see it now: keeping the breadth, and making it useful,
in rooms built around depth.&lt;/p&gt;
&lt;h2&gt;What I&apos;m trying to do about it&lt;/h2&gt;
&lt;p&gt;These aren&apos;t rules. They&apos;re what I&apos;m practising, with mixed success.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Separating &quot;I don&apos;t know this&quot; from &quot;I don&apos;t belong here&quot;.&lt;/strong&gt; The first is
usually true and usually fixable: I can ask, read, or rely on someone who
knows. The second is a story I tell myself, and it&apos;s rarely backed by any
evidence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Being the least expert in the room, on purpose.&lt;/strong&gt; I used to see it as a
risk. Now I try to see it as the best seat for learning. Sitting with people
who are far better than me at one thing is how my &quot;okay&quot; slowly becomes
&quot;good&quot; across many things. It also changes how I lead: when I&apos;m not the
expert, I have to listen, and let the people who know decide what they&apos;re
best placed to decide.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Asking the question anyway.&lt;/strong&gt; &quot;Sorry, can you explain that part?&quot; costs a
moment of feeling exposed. Not asking costs weeks of building on something I
misunderstood. Half the time, someone else in the room wanted to ask it too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Leaning into what comes naturally.&lt;/strong&gt; I&apos;ve spent time trying to become great
at things I&apos;ll never be great at, because a role description or someone I
admired said I should. Some weaknesses do need to get to &quot;good enough&quot;. Past
that, I&apos;m learning to invest more in what I&apos;m already good at: the curiosity,
the connecting, the translating. And to look for the places where that&apos;s
exactly what&apos;s needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keeping a short list of things that went well.&lt;/strong&gt; On the days the voice is
loud, facts help more than reassurance.&lt;/p&gt;
&lt;h2&gt;If you&apos;re the generalist in a room of experts&lt;/h2&gt;
&lt;p&gt;You&apos;re probably not the only one feeling this, even if nobody says it. Being
okay at many things isn&apos;t a failure to specialise. In the right place, it&apos;s
what holds the specialists&apos; work together.&lt;/p&gt;
&lt;p&gt;I&apos;m still learning this, and I expect to keep learning it for a while. If it
resonates, I&apos;d like to hear how you deal with it.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The book: David Epstein, Range: Why Generalists Triumph in a Specialized
World (2019).&lt;/em&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.wnJpD9yp.png"/><enclosure url="/_astro/cover.wnJpD9yp.png"/></item><item><title>Moneytrac: one honest picture of everything I own</title><link>https://lollilog.com/blog/moneytrac-intro</link><guid isPermaLink="true">https://lollilog.com/blog/moneytrac-intro</guid><description>Why I built a self-hosted tracker for investments, pensions and cash across brokers and currencies, what it does, and the decisions behind it.</description><pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Image } from &apos;astro:assets&apos;&lt;/p&gt;
&lt;p&gt;import phone from &apos;./phone.png&apos;&lt;/p&gt;
&lt;p&gt;My savings live in too many places: a couple of brokers, a pension, cash
accounts, some in euro and some in dollars. Each provider shows its own
slice, in its own way, with its own idea of what &quot;performance&quot; means. For
years the full picture was a spreadsheet I updated when I remembered to,
which meant it was usually out of date, and occasionally just wrong.&lt;/p&gt;
&lt;p&gt;So I built &lt;strong&gt;Moneytrac&lt;/strong&gt;: a small web app, running on my own servers at
home, that brings it all together. It isn&apos;t a trading tool and it gives no
advice. It answers one question well: &lt;em&gt;what do I have, where, and how is it
doing?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This post covers what it does and the decisions behind it. Later posts go
deeper into the components and into the infrastructure it runs on.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Every screenshot here shows made-up demo data. The numbered blue markers
are mine, not part of the app.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The first screen&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://lollilog.com/_astro/overview.ZnWVo694_Zb5D6Y.webp&quot; alt=&quot;Moneytrac&amp;#x27;s Overview in dark mode, with four numbered markers: the price refresh line, the Refresh prices button, the net worth total and the Needs attention box. Made-up data.&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;When prices were fetched, and when the next refresh is.&lt;/strong&gt; Prices are
refreshed at fixed times, and the app says so instead of pretending
they&apos;re live.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refresh prices&lt;/strong&gt;, only for admins and rate-limited (more on why
below).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every total is in one currency.&lt;/strong&gt; Euros and dollars are never added
together; a dollar holding inside a euro account is converted with a
stored daily exchange rate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Problems are listed, not hidden.&lt;/strong&gt; Here a holding has no price (it was
delisted), so its value is left out and the app says so. It never
guesses a price.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;What it does&lt;/h2&gt;
&lt;h3&gt;Brings in history from what brokers already give you&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://lollilog.com/_astro/import.CkGXfLKN_Z21TXQ7.webp&quot; alt=&quot;Moneytrac&amp;#x27;s import preview in dark mode: two DEGIRO files, each with the format it was recognised as, and a Confirm import button. Made-up data.&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Each file&apos;s format is recognised on its own&lt;/strong&gt;: here a DEGIRO
transactions export...&lt;/li&gt;
&lt;li&gt;...and a DEGIRO account statement, uploaded together. Interactive
Brokers, Trading 212 and Morgan Stanley reports work too, plus a simple
CSV for anything else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nothing is saved until you confirm.&lt;/strong&gt; And importing the same file
twice never duplicates anything: every transaction is recognised by the
broker&apos;s own ID, so I can re-import whenever I like.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Works out what you hold, and keeps prices honest&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://lollilog.com/_astro/positions.B06DbJIZ_jpWaW.webp&quot; alt=&quot;The positions table of one account in dark mode, with five numbered markers: a derived position, the investment type picker, a price in US dollars, an unavailable price and the refresh button. Made-up data.&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Positions are worked out from transactions&lt;/strong&gt; (&quot;derived&quot;), so what I
hold can&apos;t quietly drift away from what I bought and sold. Things with
no transactions, like a pension balance, are typed in by hand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Investment type&lt;/strong&gt; comes from the price provider (stock, ETF, fund,
crypto...), and I can override it, for example to mark a bond ETF as
bonds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prices stay in their own currency.&lt;/strong&gt; This account is in euro; the
stock is priced in dollars and converted with that day&apos;s stored rate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every price says how fresh it is&lt;/strong&gt;: &lt;em&gt;live&lt;/em&gt;, &lt;em&gt;stale&lt;/em&gt; or &lt;em&gt;unavailable&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A manual refresh per holding&lt;/strong&gt;, for admins only.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Shows the full picture, and projects forward honestly&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://lollilog.com/_astro/projection.BWzq34Yy_Z26bbR2.webp&quot; alt=&quot;The value chart in dark mode, ten years ahead: a dashed projection line, with four numbered markers on the today line, the return, the not-a-forecast line and the growth rate. Made-up data.&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Where real data ends.&lt;/strong&gt; Everything right of this line is a
projection, drawn dashed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The return is measured from prices, not balances&lt;/strong&gt;, so money I pay in
never counts as performance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Projection, not a forecast&quot;&lt;/strong&gt; is written right under the chart,
because it isn&apos;t one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The growth rate is estimated from past prices&lt;/strong&gt;, and I can change it
to see other scenarios.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://lollilog.com/_astro/breakdown.583g4c63_Fji4t.webp&quot; alt=&quot;The breakdown by investment type in dark mode: a donut chart and a table of funds, cash, stocks, ETFs, crypto and bonds, with two numbered markers on the tabs. Made-up data.&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Five ways to slice it&lt;/strong&gt;: by account type, investment type,
institution, single holding or region.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Region is approximate&lt;/strong&gt;, and labelled that way: it&apos;s worked out from
where a holding is listed, which is a rough guide at best.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The decisions behind it&lt;/h2&gt;
&lt;h3&gt;Prices: a free source, used as if it might disappear&lt;/h3&gt;
&lt;p&gt;Prices come from Yahoo Finance. It&apos;s free, needs no key, and covers stocks,
funds and exchange rates. But it&apos;s an &lt;strong&gt;unofficial&lt;/strong&gt; service: there&apos;s no
contract, the endpoints can change, and it can block anyone who asks too
often. So I treat it as fragile and borrowed, not as something to lean on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pages never call it.&lt;/strong&gt; Opening the app only reads prices already
stored in the database. Prices are fetched in the background, at known
times, and nowhere else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fixed times, not &quot;every few minutes&quot;.&lt;/strong&gt; On weekdays there are ten
refreshes, from just after the European markets open to just after the
US markets close. &lt;strong&gt;At weekends there&apos;s one&lt;/strong&gt;, at midday: stock markets
are closed, so more would only fetch the same numbers again, while a
single refresh still catches what does move, like crypto.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;As few requests as possible.&lt;/strong&gt; Each holding&apos;s ticker and currency are
looked up once and remembered forever, and up to 20 holdings are priced
in a single request. A whole scheduled refresh of my own portfolio is
one request.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A quota for anything done by hand.&lt;/strong&gt; The Refresh prices button is for
admins only, and limited to &lt;strong&gt;6 refreshes per holding per hour&lt;/strong&gt;, counted
across all users, because Yahoo sees requests per symbol, not per user.
If the quota counter itself is unavailable, manual refresh is switched
off rather than allowed without limits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every request is logged&lt;/strong&gt;, so I can always answer &quot;how much did we ask
for today, and why?&quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Stale&quot; means a refresh was missed&lt;/strong&gt;, not &quot;old&quot;. A price fetched at
9 pm is still exactly as current as intended at 7 the next morning, so it
isn&apos;t flagged overnight. If a scheduled refresh fails, the affected
prices are marked stale within half an hour.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swappable.&lt;/strong&gt; All of this sits behind one small interface, so moving to
a paid, documented provider would mean replacing one piece, not the app.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Brokers: their official exports, not unofficial APIs&lt;/h3&gt;
&lt;p&gt;Some brokers can be connected directly, but often only through unofficial
APIs: you hand over your login (sometimes including the two-factor secret),
and the integration breaks whenever the broker changes its site. The code
for two broker connections exists, but &lt;strong&gt;it&apos;s switched off&lt;/strong&gt;, and the app
refuses to start if anyone configures it. Instead, data comes from the
&lt;strong&gt;exports brokers officially provide&lt;/strong&gt;: stable formats, no passwords stored
anywhere, and a file I can look at before importing.&lt;/p&gt;
&lt;h3&gt;Numbers: strict about what a number means&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One currency per account, never mixed.&lt;/strong&gt; Each account declares its
currency; foreign holdings inside it are converted with stored daily
rates. If a rate is missing, the total isn&apos;t shown, rather than being
shown wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing stays missing.&lt;/strong&gt; No price means no value, plus a warning,
never a guess.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance from prices.&lt;/strong&gt; Each holding&apos;s own price change, weighted by
what it&apos;s worth today. A deposit isn&apos;t a gain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safe to repeat.&lt;/strong&gt; Imports, syncs and rebuilds can all run twice without
changing anything the second time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Privacy: my data stays at home&lt;/h3&gt;
&lt;p&gt;It runs on a small cluster in my house. The price provider only ever learns
&lt;strong&gt;which&lt;/strong&gt; securities to price, never who holds them or how many. Each user&apos;s
data is separated in every database query, and asking for someone else&apos;s
account gets &quot;not found&quot;, not even &quot;forbidden&quot;.&lt;/p&gt;
&lt;h3&gt;Written down as it&apos;s built&lt;/h3&gt;
&lt;p&gt;Every decision, known gap and procedure is documented: what the app does,
why, how to release it and how to roll it back. Someone new, or me in a
year, can pick it up without asking anyone.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The hard part wasn&apos;t the charts. It was being strict about what a number
means: which currency it&apos;s in, how old its price is, and whether a &quot;gain&quot;
is a market move or just a deposit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;How it&apos;s built&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./architecture.svg&quot; alt=&quot;Moneytrac&amp;#x27;s architecture: the browser talks only to the web app, which passes requests to the Go API; the API and a Go background worker keep everything in MongoDB; Redis holds counters for the API; the worker fetches prices from Yahoo Finance and OpenFIGI outside the cluster; metrics, logs and traces go to Prometheus, Loki and Tempo, shown in Grafana.&quot;&gt;&lt;/p&gt;
&lt;p&gt;Five small pieces, each with one job:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Web app&lt;/strong&gt; (SvelteKit), installable on a phone like an app. The browser
only ever talks to the app&apos;s own address; the app&apos;s server passes
requests on to the API. No cross-site calls, nothing for a phone&apos;s
browser to block, and the API is never exposed directly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API&lt;/strong&gt; (Go): login, accounts, imports, and every calculation that needs
stored data. It never calls the price provider while you wait, except
for a holding it has never seen and an admin&apos;s manual refresh.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background worker&lt;/strong&gt; (Go too): the scheduled price refreshes, the daily
snapshot that feeds the charts, and pricing new holdings as soon as they
appear.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MongoDB&lt;/strong&gt;: everything that matters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redis&lt;/strong&gt;: a few counters that are fine to lose.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each gets a deep dive later in the series. Here&apos;s why the two databases
are the way they are.&lt;/p&gt;
&lt;h3&gt;Why MongoDB is good enough&lt;/h3&gt;
&lt;p&gt;The honest answer is that the data is small and the questions are simple.
One person&apos;s finances are a handful of accounts, some hundreds or
thousands of transactions, and a daily price for each security held:
about 2,500 price points per security for ten years. Every question the app asks is &quot;this user&apos;s
accounts&quot; or &quot;this account&apos;s transactions&quot;; there&apos;s no cross-user
reporting and no big joins.&lt;/p&gt;
&lt;p&gt;For that shape, MongoDB gives me what I actually need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Guarantees from indexes, not from code.&lt;/strong&gt; A transaction is unique per
broker and broker ID, a position per account and symbol. The database
refuses duplicates, which is what makes re-importing the same file safe.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A time-series collection&lt;/strong&gt; for the daily snapshots behind every chart,
built in, with no extra service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Additive changes for free.&lt;/strong&gt; New fields and collections are simply
ignored by older code, so any earlier version can run on today&apos;s data
and a rollback never needs a data migration. I keep it that way on
purpose: data is only ever added in one release and cleaned up in a
later one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Atomic single-document writes&lt;/strong&gt; cover every update the app makes; it
doesn&apos;t need multi-document transactions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Would PostgreSQL work? Of course, and if the app ever grows into real
multi-user reporting, a relational database is where I&apos;d look first. At
this size the difference doesn&apos;t matter, and the simplest thing that
gives the guarantees above wins.&lt;/p&gt;
&lt;h3&gt;Redis: needed, or just familiar?&lt;/h3&gt;
&lt;p&gt;Redis was originally a price cache: any page load could fetch prices,
cached for ten minutes. When prices moved into MongoDB (they have to
survive restarts once they&apos;re fetched only a few times a day), the cache
went, but Redis stayed for one kind of job: &lt;strong&gt;counters that must be
checked and updated in one step, by many requests at once&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The manual-refresh quota&lt;/strong&gt;: at most 6 refreshes per holding per hour.
Two clicks at the same moment must not both take the last slot. A small
script runs inside Redis that drops old entries, checks the count and
takes a slot in a single call, and says exactly when the next slot frees
up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Login and signup limits&lt;/strong&gt;: attempts per network address, emails per
email address, and a temporary lockout after repeated failed logins,
shared by every copy of the API.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&apos;s the case for it. Looking at it critically, it&apos;s weaker than it
sounds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It&apos;s a whole extra service for two small jobs&lt;/strong&gt;: another container to
run, update, monitor and back up, another thing that can be down. It
even has its own replicated disk, for data I describe as disposable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MongoDB could do both.&lt;/strong&gt; A single document per holding, updated with
one conditional write (&quot;add a timestamp only if there are fewer than six
recent ones&quot;), is just as atomic. Expiring counters are what MongoDB&apos;s
TTL indexes are for, and the app already uses them for sessions and
emailed links. The original reason for picking Redis, that the check and
the update must happen in one step, doesn&apos;t actually require Redis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Its speed doesn&apos;t matter here.&lt;/strong&gt; A few logins and the odd refresh
click a day are nowhere near what MongoDB handles without noticing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Shared by every copy of the API&quot; is a future benefit.&lt;/strong&gt; Today there
is one copy of the API, and other parts (import previews kept in
memory) already stop me from running two.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The failure mode is uncomfortable.&lt;/strong&gt; If Redis is down, the login
limits simply stop applying, so the app keeps working rather than
locking everyone out. That&apos;s a deliberate choice, but it means the
protection can disappear silently, and nothing alerts me about it yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So why keep it? It works, it&apos;s tested, at home it costs nothing to run,
and it&apos;s the standard tool if the app ever needs several API copies. But
if I started again today, I&apos;d begin with MongoDB alone and add Redis only
when a measurement asked for it. Moving the counters into MongoDB, and
removing a service, is on the list.&lt;/p&gt;
&lt;h3&gt;What running at home made easy&lt;/h3&gt;
&lt;p&gt;Some choices were obvious only because it runs on my own small Kubernetes
cluster instead of a cloud account:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No bill per service.&lt;/strong&gt; Running my own MongoDB, Redis, Prometheus, Loki
and Tempo costs nothing extra, so the app had full metrics, logs and
traces from early on, instead of &quot;later, when it&apos;s worth paying for&quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A permanent staging copy&lt;/strong&gt; with made-up data, always on, at no cost.
Every change runs there first; production only moves when I merge a
release.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One copy of each database is enough.&lt;/strong&gt; The disks underneath are
replicated across two machines and backed up nightly, a restart takes
seconds, and a few minutes of downtime on a personal app is fine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nothing exposed directly.&lt;/strong&gt; The app is reached through a Cloudflare
tunnel, with no open ports on my router, and it sits behind a login
wall until it&apos;s ready for anyone else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;My data never leaves the house&lt;/strong&gt;, apart from the names of the
securities to price.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How all of that is set up, from the cluster to automatic deployments, is
its own post.&lt;/p&gt;
&lt;h3&gt;Next: measuring instead of guessing&lt;/h3&gt;
&lt;p&gt;Because every request carries one trace ID, from the web app&apos;s server
through the API down to each database query, I can see where the time
goes instead of guessing. The first fix that came out of it: pages that
value many holdings were reading prices one at a time. Loading them in
batches, with a small per-request cache, took the same set of requests on
the staging copy from 273 database reads to 181, net worth from 21 to 6 ms and the
breakdown from 41 to 6 ms, with every response identical before and after.&lt;/p&gt;
&lt;p&gt;The slowest part now is the long-range chart: its median is around a
tenth of a second, but about one request in four takes a quarter to half a
second. A trace of one such request shows most of the time going into loading and
decoding the stored points; the database query itself is only about a
quarter of it. The options
are clear (keep exchange-rate history in memory, store pre-summarised
weekly points for old history, or one history request for all accounts),
and the plan is to compare traces before and after each one rather than
pick by instinct.&lt;/p&gt;
&lt;p&gt;Two more things are still to come, each in its own article:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;End-to-end performance&lt;/strong&gt;: what a reader actually waits for, from tap
to chart, not just the server&apos;s share.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Load and stress testing&lt;/strong&gt;: pushing it with many simulated users to see
what breaks first. I already know a couple of candidates: import
previews live in the API&apos;s memory, which rules out running two copies of
the API, and there&apos;s a single MongoDB.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;It works on a phone, too&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;It installs like an app from the browser, so checking where things stand
takes a few seconds, wherever I am.&lt;/p&gt;
&lt;h2&gt;Next in this series&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Building it with an AI pair programmer&lt;/strong&gt;: what worked, what didn&apos;t, and
the rules that kept it on track.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The components in depth&lt;/strong&gt;: Go, MongoDB, Redis and SvelteKit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Getting the money maths right&lt;/strong&gt;: currencies, returns, and realised
versus unrealised profit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The infrastructure&lt;/strong&gt;: a home Kubernetes cluster, deployments from git,
and a staging copy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability for a one-person app&lt;/strong&gt;: knowing what&apos;s going on without
collecting anything personal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Finding a performance bug with data, not guesses&lt;/strong&gt;: the trace
comparison above, in detail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End-to-end performance and load testing.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/cover.BkYtdQRG.png"/><enclosure url="/_astro/cover.BkYtdQRG.png"/></item></channel></rss>