<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Dan Hartropp</title>
    <link>https://www.danhartropp.com/index.html</link>
    <description>Tech pragmatist, sometime startup CTO and once-upon-a-time digital artist.</description>
    <language>en-gb</language>
    <atom:link href="https://www.danhartropp.com/feed.xml" rel="self" type="application/rss+xml"/>
    
    <item>
      <title>[Brutally] optimising for cost</title>
      <link>https://www.danhartropp.com/read/brutally_optimising_for_cost.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/read/brutally_optimising_for_cost.html</guid>
      <category>read</category>
      <pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate>
      <description>serverless is cheap until it isn&#39;t ... so design for the price list</description>
      
      <content:encoded>&lt;p&gt;Serverless compute is a wonderful deal when you&#39;re small. You only pay for what you use, it all scales to zero when not in use, and the free tiers are generous enough that a side project can live on them indefinitely. The catch is that the bill scales linearly with usage, and the number of operations a system performs has a habit of multiplying in ways nobody notices until the invoice arrives.&lt;/p&gt;
&lt;p&gt;I&#39;ve recently spent some time building a frost and rain alerting service at &lt;a href=&#34;https://weather.danhartropp.com&#34;&gt;weather.danhartropp.com&lt;/a&gt;, and I set myself a deliberately unreasonable target from the start: it had to be able to run a million forecasts every night without the bill becoming a problem ... and the million users should all arrive in the same month. I don&#39;t expect anything like that traffic, but designing for that kind of scale now should prevent an annoying bill later. It should also help prevent the far more likely case, which is just enough users to tip me over the free tier and start paying for it out of my own pocket.&lt;/p&gt;
&lt;p&gt;The bottom line: if you know where your costs scale (per request, per user per day, or per byte) you can usually arrange the design so the expensive multiplier never arrives, and for bursty/batchy operations you might be able to stay inside a free tier. If you don&#39;t know, the default patterns will quietly pick the most expensive option for you. It&#39;s mostly just reading the pricing page and doing a few sums before writing the code. &lt;/p&gt;
&lt;h2&gt;What the app actually does&lt;/h2&gt;
&lt;p&gt;Each day, a batch job downloads the Met Office&#39;s UK forecast, which is published as open data. For every place someone has subscribed to, it samples the forecast at that point, adds a set of terrain descriptors (how sheltered it is, whether cold air pools there, that sort of thing), and runs two small machine learning models that predict how much colder the ground will get than the Met Office forecast says. If the answer crosses the frost threshold, the subscriber gets a push notification. Rain works in a similar way but is simpler, because the Met Office already publishes a probability for 8,667 sites across the UK ... so the job just looks up the nearest one and applies a calibration curve.&lt;/p&gt;
&lt;p&gt;This means there are two very different halves to the system. There&#39;s a small website where people search for a place, drop a pin on a map and subscribe, and there&#39;s a batch job that runs twice a day and does the actual forecasting.&lt;/p&gt;
&lt;h2&gt;Where the money goes when Hacker News finds you&lt;/h2&gt;
&lt;p&gt;The (imaginary) scenario I used to stress-test the design was the classic hug of death ... a link lands on the front page of something like Hacker News and a few hours later a very large number of people have all arrived at once. The typical costs of what happens next break down like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What a visitor does/gets&lt;/th&gt;
&lt;th&gt;What it touches&lt;/th&gt;
&lt;th&gt;What you&#39;re charged for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;loads a page&lt;/td&gt;
&lt;td&gt;the web server&lt;/td&gt;
&lt;td&gt;requests and CPU-seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;searches for a place&lt;/td&gt;
&lt;td&gt;a geocoding service&lt;/td&gt;
&lt;td&gt;requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;looks at the map&lt;/td&gt;
&lt;td&gt;map tiles&lt;/td&gt;
&lt;td&gt;bytes sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;subscribes&lt;/td&gt;
&lt;td&gt;the database&lt;/td&gt;
&lt;td&gt;writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a forecast every night from then on&lt;/td&gt;
&lt;td&gt;the batch job&lt;/td&gt;
&lt;td&gt;reads, writes and compute, per subscriber&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The first four rows are the one-off spike, and they are where things will visibly fail if the budget gets blown. The last row is the one that matters, because &lt;strong&gt;a hug of death in this case is a brief spike in the request path but a permanent step in the batch path.&lt;/strong&gt; Every subscriber you gain is a database read, a forecast and a write every single night from then on.&lt;/p&gt;
&lt;h2&gt;One big read beats a million small ones&lt;/h2&gt;
&lt;p&gt;The subscriptions live in Firestore, Google&#39;s serverless document database, which charges per document read and written. The obvious design for the nightly job is to read each subscription, forecast it, and write each result back as a document. At a million subscribers that&#39;s a million reads and a million writes every night.&lt;/p&gt;
&lt;p&gt;Firestore is cheap per operation, so this doesn&#39;t sound like much. A nightly full scan is about &lt;strong&gt;$9 a month&lt;/strong&gt; and writing the results back costs about &lt;strong&gt;$27 a month&lt;/strong&gt; more. It&#39;s not a lot, but I would somewhat resent paying hundreds a year to send other people weather alerts when there&#39;s a better way. &lt;/p&gt;
&lt;p&gt;Instead, the job writes its results as one file in cloud storage (Google Cloud Storage, which charges per object rather than per row). A night&#39;s forecasts for a million points come to about 40 MB compressed, written as a single object. That&#39;s &lt;strong&gt;one billable operation instead of a million.&lt;/strong&gt; It&#39;s also a far better format for the job that really needs it, because those files double as the training data for the next version of the model.&lt;/p&gt;
&lt;p&gt;The reads take a bit more thought. The job also writes a snapshot of the subscriptions it used, and the next run starts from that snapshot and only asks Firestore for documents that have changed since. Even on a really busy day that&#39;s about 30,000 documents rather than a million, so &lt;strong&gt;a 97% reduction&lt;/strong&gt;, and at those volumes it costs essentially nothing. Materialising the data like this is a useful trick, provided that nothing is ever deleted. A hard delete would be invisible to the batch job, and the user would carry on getting alerts after they&#39;d unsubscribed. Soft deleting (marking the row as &#34;deleted&#34;) solves this problem. &lt;/p&gt;
&lt;p&gt;It&#39;s worth noting that the compute isn&#39;t the expensive part either. Both frost models together take about 180 microseconds per point, so a million points is about three minutes of a single CPU core. The whole batch fits on one machine, well inside Cloud Run&#39;s free allowance of 180,000 vCPU-seconds a month. In fact the job &lt;strong&gt;can&#39;t&lt;/strong&gt; usefully be spread across more machines, because the Met Office publishes whole-UK grids (around 500 MB a run), and every extra machine would just download the same files again.&lt;/p&gt;
&lt;h2&gt;Don&#39;t pay for everyone opening the notification at once&lt;/h2&gt;
&lt;p&gt;The other spike in this system is self-inflicted. When a frost alert goes out, thousands of people get a notification at roughly the same time, and a good number of them tap it. If the page behind that tap looked up the forecast or the subscription, every notification sent would become a database read at exactly the moment of peak load. So it doesn&#39;t. &lt;strong&gt;The alert page is built entirely from its own URL&lt;/strong&gt;: the kind of alert, the place name, the probability and the temperature all travel in the link. Opening it touches no database and no storage at all, which means it costs effectively nothing however many people open it.&lt;/p&gt;
&lt;p&gt;There&#39;s a security cost to this (anything in a URL can be edited by whoever has it), so every value is checked against a hard-coded fixed list or a valid range, and anything that doesn&#39;t parse is dropped rather than displayed. Viewing the place on a map does need a database read, so that&#39;s a link the reader has to click rather than something every recipient gets automatically.&lt;/p&gt;
&lt;h2&gt;Put the heavy bytes where they&#39;re cheap&lt;/h2&gt;
&lt;p&gt;Most of the site is tiny. The pages are rendered on the server and weigh between 851 bytes and 2.5 KB, and a first visit including the stylesheet is about &lt;strong&gt;1.6 KB.&lt;/strong&gt; Everything works with JavaScript switched off, and a single server instance handled about 575 requests a second (that&#39;s about 50 million views a day) in load testing at 20% CPU. A massive front-page traffic spike is well within what one small instance can absorb, and the service scales to zero when nobody&#39;s using it.&lt;/p&gt;
&lt;p&gt;The map is the exception. The mapping library alone is about 300 KB compressed (two orders of magnitude more than the rest of the site) and map tiles are where traffic really adds up. GCP has quite high cloud egress charges, so as well as making sure the map only loads on the one page that needs it, the tiles come from a single file of UK map data hosted on Cloudflare R2, which doesn&#39;t charge for data transfer. The browser then uses range requests to read just the parts of that file it needs. There&#39;s no map provider, no API key and no per-tile fee. Thank you Cloudflare!&lt;/p&gt;
&lt;h2&gt;A brief diversion into geocoding&lt;/h2&gt;
&lt;p&gt;Searching for places is the one part of the site that relies on a third party, and it turned out to be the part with the nastiest surprise in the terms of service. Most geocoding providers have a generous free tier, but it only covers displaying results temporarily. Storing coordinates you got from them, which is exactly what a subscription is, needs a paid licence. For the provider I started with, that was &lt;strong&gt;$5 per 1,000 requests with no free tier&lt;/strong&gt;, which at a million sign-ups a month is roughly &lt;strong&gt;$5,000 a month&lt;/strong&gt; for a service whose entire batch side costs tens of dollars. Again, not likely I&#39;d ever get that much traffic, but it would be a bad month if I did ... and even a fraction of that would be noticeable. &lt;/p&gt;
&lt;p&gt;I looked at self-hosting the open-source alternatives (which needed a server running permanently at $30–70 a month), but in the end I built my own UK place search from open data (Ordnance Survey, ONS and OpenStreetMap), which runs on the same scale-to-zero model as everything else. The free tier covers about 25 busy instance-hours a month, which at measured throughput is several million searches, and the residual cost is a pound or two a month for storage and data transfer. &lt;/p&gt;
&lt;p&gt;The geosearch engine is still under active development (inferring intent from ambiguous queries is a surprisingly interesting problem), so that&#39;s a story for another post.&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>Giving an LLM a city to run (sort of)</title>
      <link>https://www.danhartropp.com/read/we_gave_a_frontier_model_a_city_to_run.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/read/we_gave_a_frontier_model_a_city_to_run.html</guid>
      <category>read</category>
      <pubDate>Mon, 07 Sep 2026 00:00:00 +0000</pubDate>
      <description>can a frontier model beat a trained specialist on a &#34;reasoning&#34; task?</description>
      
      <content:encoded>&lt;p&gt;Frontier models are going into jobs that used to need a purpose-built system. The pitch is attractive: just describe the task and plug in reasoning. This is obviously much easier than developing your own systems and models from scratch and drops the cost of deployment to near zero. But does it really work?&lt;/p&gt;
&lt;p&gt;Weirdly, there aren&#39;t many people who are keen to run experiments on real-world business processes, so I thought I&#39;d try the next best thing ... giving the model a [sim] city to play with.&lt;/p&gt;
&lt;p&gt;I also used reinforcement learning to train a specialist model from scratch to see if a frontier model could beat it.&lt;/p&gt;
&lt;p&gt;The bottom line: The frontier model can tell you exactly how to run the city. It talks a really good game. But it fails to execute and can&#39;t even follow its own plan.&lt;/p&gt;
&lt;p&gt;So, to wildly extrapolate a takeaway for the people who won&#39;t read any further than this ... if you can define a task accurately, a specially-trained model is likely to beat a frontier LLM. If you can&#39;t define the task accurately and you&#39;re happy to settle for mediocre performance, then an LLM will work. Just don&#39;t trust what it says it&#39;s doing.&lt;/p&gt;
&lt;h2&gt;The setup&lt;/h2&gt;
&lt;p&gt;I used Micropolis, an open-source SimCity engine. You get a 32×32 patch of land, a million in funds, and 200 decisions. Each turn you place one thing: a house, a road, a power line, a factory. Your score is the population at the end.&lt;/p&gt;
&lt;p&gt;It&#39;s a good task for this experiment because nothing pays off at once. A zone is dead ground until a road reaches it and power arrives. Growth compounds, so a decision on turn 10 is worth several on turn 80. You have to sequence, not just choose ... and that requires some form of reasoning.&lt;/p&gt;
&lt;p&gt;I trained a specialist model on the simulated city using a tiny convolutional network (about 94K parameters) and a hundred thousand games of practice, played inside a reinforcement learning loop on a laptop GPU. The frontier model (Claude Opus 5) was given tools to interact with the city and no training at all.&lt;/p&gt;
&lt;p&gt;For the sake of an independent baseline, I also wrote a dumb heuristic rule ... build a power plant, then alternate road, power, residential and industrial (1:3 ratio) zones until the game ends.&lt;/p&gt;
&lt;h2&gt;What they built&lt;/h2&gt;
&lt;p&gt;Here is what the specialist builds. Yes, it&#39;s ugly.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;A dense, fully developed city built by the trained specialist&#34; src=&#34;/static/img/micropolis/city-trained-calm.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Dense usage, roads to everything and power everywhere. It&#39;s not pretty but it has found a layout that packs the most houses (and other necessary services) between the fewest roads. Note the lack of fire stations. That will become important later. &lt;/p&gt;
&lt;p&gt;Here is the city Claude built, on the same map.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;The frontier model&#39;s city: zones along long road spines, most still empty plots&#34; src=&#34;/static/img/micropolis/city-llm-calm.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;It&#39;s a logical layout. &lt;strong&gt;But most of these plots never became anything.&lt;/strong&gt; The specialist&#39;s map is full of buildings. This one is full of zoning — flat squares marked &lt;code&gt;R&lt;/code&gt;, laid out correctly, connected to road, connected to power, and empty. Lots of fire stations though. &lt;/p&gt;
&lt;h2&gt;The LLM is a mediocre reasoner&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The LLM scored 427 against the specialist&#39;s 951.&lt;/strong&gt; A dumb rule scores 71, so the LLM beats that at least, but is only about half as good as the specialist.&lt;/p&gt;
&lt;p&gt;The LLM is not failing at the mechanics. It knows a zone needs a road and it gives it one. Just one building in the whole city ends up unpowered. In fact the LLM follows the rules &lt;em&gt;better&lt;/em&gt; than the specialist does:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;valid moves&lt;/th&gt;
&lt;th&gt;zones built&lt;/th&gt;
&lt;th&gt;people per zone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;specialist&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13.2&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;frontier model&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99.5%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Nearly every move it makes is legal and lands. It builds a little less, and gets half as much out of each one.&lt;/p&gt;
&lt;h2&gt;The LLM can state the strategy. It can&#39;t execute it.&lt;/h2&gt;
&lt;p&gt;This is the most important finding for me.&lt;/p&gt;
&lt;p&gt;Before the LLM saw a map, I asked the LLM to write down its plan. It can do this because it will have come across Micropolis (and Sim City) in its training data. Part of the plan read:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&#34;Mix: near-even Residential/Commercial/Industrial early because industry and commerce create residential demand; after turn ~50 skew residential, which holds the most people.&#34;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is exactly right and is basically what the specialist model built. In Micropolis, people arrive because there is work. Residential demand therefore comes from shops and factories. If you just build houses with no economy, the houses stay empty.&lt;/p&gt;
&lt;p&gt;Here is what each model actually built:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;houses&lt;/th&gt;
&lt;th&gt;shops&lt;/th&gt;
&lt;th&gt;factories&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;specialist&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;27&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;frontier model&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The LLM wrote the rule about jobs and then zoned a dormitory suburb. Its city ended with 327 residents and 44 jobs. The plots stayed empty because there was nothing for anyone to do.&lt;/p&gt;
&lt;p&gt;It had the same problem with the concept of returns compounding over time. It said growth compounds and early zones are worth more, but it actually built half as many of them early.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;zones down by turn 50&lt;/th&gt;
&lt;th&gt;road tiles per zone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;specialist&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.9&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;frontier model&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.9&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;It wasn&#39;t great at building roads either. The specialist lines development along both sides of one road and lets neighbouring zones pass power to each other. The model ran long spines and hung less off them. Half its turns went on building plumbing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The gap is not knowledge. It is the distance between describing a plan and following one for two hundred decisions.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To give the LLM a fair chance, I let it play again, giving it a full record of every move it had made and whether each one worked, and allowed it to deviate from its plan. In effect, allowing it to learn as it played. Its execution went to near-perfect, but its score got worse, dropping from 443 to 427.&lt;/p&gt;
&lt;p&gt;This is, of course, how today&#39;s models work, not a permanent fact about machines. The next generation may not have these problems. But for now, &lt;strong&gt;a specialist trained to execute beats a model that sounds like it knows what it is doing&lt;/strong&gt; — as long as you can build a good enough model of the world to train it against. This is important because ...&lt;/p&gt;
&lt;h2&gt;What if we set the city on fire?&lt;/h2&gt;
&lt;p&gt;A specialist is only a specialist in the conditions it trained for. So I lit twelve fires during each game, at fixed times, in random places. The specialist&#39;s training didn&#39;t include fires at all and it was awful at dealing with them.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;The same specialist under fire: sparse development, burnt ground, active fires&#34; src=&#34;/static/img/micropolis/city-trained-fires.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;951 to 56.&lt;/strong&gt; Scorched ground and fires still burning, barely a district left standing. The specialist does not fight them - it doesn&#39;t even know what they are, so it goes on placing houses beside buildings that are alight.&lt;/p&gt;
&lt;p&gt;In contrast, the LLM scored &lt;strong&gt;168&lt;/strong&gt; when the fires were turned on. So it fell too, but it did not collapse, and it beat the specialist easily.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is the real case for general reasoning in production.&lt;/strong&gt; It&#39;s not that the &#34;reasoning&#34; of today&#39;s LLMs is better, it&#39;s that LLMs have read about the world. They know what a fire is without being told, so what utterly destroys a specialist model merely costs a lot for an LLM. To put it another way, the failure that hurts you in a live system is the one nobody planned for, and that is where the specialist can&#39;t help you.&lt;/p&gt;
&lt;h2&gt;Sixteen minutes later ...&lt;/h2&gt;
&lt;p&gt;I took the same broken specialist model and trained it again with the fires switched on. It took just sixteen minutes to converge on one laptop GPU.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;The retrained specialist under fire: a developed city studded with fire stations&#34; src=&#34;/static/img/micropolis/city-firetrained-fires.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;56 to 579.&lt;/strong&gt; Three and a half times the LLM&#39;s score.&lt;/p&gt;
&lt;p&gt;You can see what it learned. The red buildings marked &lt;code&gt;FD&lt;/code&gt; are fire stations. They are everywhere. It builds three times as many as before and cuts police to pay for them. It worked that out for itself from two hundred games of burning cities down.&lt;/p&gt;
&lt;p&gt;Importantly, the specialist did not become useless the rest of the time. In normal conditions it still scores 833 — twelve per cent below where it started, for a tenfold gain when things go wrong.&lt;/p&gt;
&lt;h2&gt;What this means if you are deploying&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;A frontier model is mediocre, but adaptable and easy to deploy.&lt;/strong&gt; The LLM scored half the performance of a purpose-built specialist system, but was deployable in about 5 minutes, just prompt-and-go.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Saying the answer is not doing it.&lt;/strong&gt; The LLM wrote the right strategy and then ignored it. Judge a system on what it does over a long run of decisions, not on how well it explains itself - or risk getting bitten in production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The LLM advantage is robustness, not capability.&lt;/strong&gt; The LLM wins on the conditions you did not plan for. That is insurance, but it costs performance on the conditions you &lt;strong&gt;did&lt;/strong&gt; plan for.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Closing a gap once you know about it is quick.&lt;/strong&gt; Just sixteen minutes in this case. That was the cost of teaching a specialist about fire once fire became a thing. Training (and especially re-training) the model is not expensive.&lt;/p&gt;
&lt;p&gt;It&#39;s worth noting that the specialist was also thousands of times cheaper to run. It played a full 200-turn game in &lt;strong&gt;0.42 seconds&lt;/strong&gt;. The LLM took &lt;strong&gt;42 minutes&lt;/strong&gt;. To save you doing the calculation, that&#39;s six thousand times slower, every game, forever. Frontier LLM providers typically make you pay per-token to use them as well.&lt;/p&gt;
&lt;p&gt;So the real question is not &#34;model or specialist&#34;. It&#39;s: &lt;strong&gt;can you list the conditions your system will meet in the real world?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you can, train a specialist for them. It will be better, it will be far cheaper to run, and adding a condition you missed costs almost nothing once you&#39;ve spotted it.&lt;/p&gt;
&lt;p&gt;But if you cannot - if the things that might happen are open-ended and you will find out which ones matter only when they do - then an LLM is mediocre everywhere but won&#39;t collapse. It will cost you about half your performance and several thousand times your compute budget. Of course, if performance matters you could always go old-school and use people instead of an LLM. Just a thought. &lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;&lt;a href=&#34;https://github.com/SimHacker/MicropolisCore&#34;&gt;Micropolis&lt;/a&gt; is the GPL-3 open-source release of the original SimCity engine&lt;/em&gt;&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>Applied AI ... Who to call first?</title>
      <link>https://www.danhartropp.com/read/who_to_call_first.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/read/who_to_call_first.html</guid>
      <category>read</category>
      <pubDate>Thu, 20 Aug 2026 00:00:00 +0000</pubDate>
      <description>can you spot the firms with the most to gain from the outside?</description>
      
      <content:encoded>&lt;p&gt;AI is going into all sorts of business processes at the moment, with results that range from
transformative to embarrassing. Most of that variation isn&#39;t in the technology, it&#39;s in the
choice of where to point it. Operations Research has spent about eighty years on that question,
but it almost always answers it from the inside, using a client&#39;s private data and the people
who already know the business.&lt;/p&gt;
&lt;p&gt;I wanted to know whether you could do it from the outside instead. Every UK company files
accounts, those accounts are public and free, and there are a couple of million of them ... so
could you work out which firms have the most to gain before you&#39;ve spoken to any of them?&lt;/p&gt;
&lt;p&gt;The bottom line: yes, up to a point. I can rank an industry by how much headroom each firm is
carrying, and I can name the real competitors that prove the headroom is reachable. What I can&#39;t
tell you is whether any particular firm will actually get there - you can only see that from the inside.&lt;/p&gt;
&lt;p&gt;The tool for the job here is Data Envelopment Analysis, a linear programme that takes the best performers in a
peer group, joins them into a frontier, and measures everyone else by the distance back to that
line. It&#39;s asking how much better a firm could do, given what its peers already manage.&lt;/p&gt;
&lt;h2&gt;The idea in one picture&lt;/h2&gt;
&lt;p&gt;Every firm in a given industry is plotted by what it earns from each resource, with return on capital along
the bottom and profit per employee up the side. Up and to the right is better.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;The efficient frontier for one sector&#34; src=&#34;/static/img/or-screener/frontier.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;The blue line is the frontier, and the firms sitting on it beat everyone else on both measures
at once. Everything behind the line is a candidate and the arrow is the gap. Because the
frontier is built from real companies you can also blend them, which lets you ask a rather more
pointed question ... how well would this firm do if you rebuilt it out of the best parts of its
competitors?&lt;/p&gt;
&lt;p&gt;Here&#39;s the answer for one care home operator, straight out of the report. &lt;strong&gt;Florence Care
Reading, 81%. Grove Care, 11%. Acorn Lodge, 8%.&lt;/strong&gt; Those are three real firms blended into a
single composite, and that composite holds no more assets and employs no more staff than the
operator being measured. The composite just earns more - or it would if it were a real firm.&lt;/p&gt;
&lt;h2&gt;What you actually get from a filing&lt;/h2&gt;
&lt;p&gt;Companies House publishes around 10,400 filings a day, tagged and machine-readable, and most of
them are a balance sheet and very little else. Employee numbers are almost always in there.
Operating profit is tagged less than half the time, which is awkward for a model that exists to
explain profit.&lt;/p&gt;
&lt;p&gt;The bigger problem is that some of the numbers are simply wrong. As an example, one motor group filed
&lt;strong&gt;427,000 employees&lt;/strong&gt; against £273m of turnover, which would make a regional car dealer a larger
employer than Tesco. Its own accounts give it away, because an untagged note further down splits
that same headcount into 158 sales, 209 aftersales and 60 administration. The filing software
had applied the document&#39;s thousands scaling to a figure that was already absolute, and about
sixty firms in the panel managed the same trick. &lt;/p&gt;
&lt;p&gt;A bit of noise matters more than it sounds, because DEA is deterministic. A firm like that doesn&#39;t just
misreport itself ... it lands on the frontier, and then every honest competitor in the sector
gets measured against a fiction. In total, I reckon about 85% of time this little project took was spent exploring, cleaning, reconciling, sanitising and double-checking the raw data. &lt;/p&gt;
&lt;h2&gt;The model&lt;/h2&gt;
&lt;p&gt;The output is operating profit and the inputs are total assets and headcount. That&#39;s all. Every extra input you add
makes each firm look a little more like a special case, until eventually nothing can be compared
with anything. Two inputs, one output ... doesn&#39;t really get much simpler and its barely a model. &lt;/p&gt;
&lt;p&gt;About a third of these firms report a loss and classical DEA needs positive numbers, so I used
the Range Directional Model instead. It works in differences rather than ratios, which lets the
losses go in as they are and keeps the question the right way up.&lt;/p&gt;
&lt;p&gt;Comparability is handled as a constraint inside the programme rather than as a filter in front
of it. Every firm gets scored against its whole sector, but only certain firms are allowed to be
its benchmark ... not its own subsidiaries, not firms an order of magnitude smaller, and not
firms whose margin is so different it suggests they&#39;re really in another business. The report then names every
benchmark firm and its weight, which matters because SIC codes are self-declared at
incorporation and effectively never corrected. &lt;/p&gt;
&lt;h2&gt;Some industries have nothing to find&lt;/h2&gt;
&lt;p&gt;I ran the whole register, which came to &lt;strong&gt;19,010 datapoints over 14,460 companies in 156
sectors&lt;/strong&gt;, out of a panel of about 62,000. Industries turn out to bunch very differently. Some companies declare more than one SIC code and I&#39;ve scored them in each industry, which is why there are more datapoints than actual companies. &lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SIC&lt;/th&gt;
&lt;th&gt;Sector&lt;/th&gt;
&lt;th style=&#34;text-align: right;&#34;&gt;Firms&lt;/th&gt;
&lt;th style=&#34;text-align: right;&#34;&gt;Median eff.&lt;/th&gt;
&lt;th style=&#34;text-align: right;&#34;&gt;% at frontier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50200&lt;/td&gt;
&lt;td&gt;Sea and coastal freight&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;40&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;0.96&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;47300&lt;/td&gt;
&lt;td&gt;Retail sale of automotive fuel&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;41&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;0.68&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;87300&lt;/td&gt;
&lt;td&gt;Residential care for the elderly&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;129&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;0.59&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;43310&lt;/td&gt;
&lt;td&gt;Plastering&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;47&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;0.59&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;36%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;56103&lt;/td&gt;
&lt;td&gt;Take-away food shops&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;107&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;0.12&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;82990&lt;/td&gt;
&lt;td&gt;Other business support&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;790&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;0.11&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;62090&lt;/td&gt;
&lt;td&gt;Other IT services&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;359&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;0.10&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90010&lt;/td&gt;
&lt;td&gt;Performing arts&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;80&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;0.08&lt;/td&gt;
&lt;td style=&#34;text-align: right;&#34;&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Sea and coastal freight sits at 0.96 with half its firms already on the frontier, I guess
because everyone owns much the same ships and runs them much the same way. It&#39;s an efficient industry and there&#39;s nothing to find there, which is worth knowing before you spend a week finding it. Performing arts sits at
0.08 and fans out across the whole range.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;Two sectors, two shapes of competition&#34; src=&#34;/static/img/or-screener/distributions.png&#34; /&gt;&lt;/p&gt;
&lt;h2&gt;Two kinds of gap&lt;/h2&gt;
&lt;p&gt;The distance back to the frontier splits into two halves, and they want completely different
conversations.&lt;/p&gt;
&lt;p&gt;The first half is slack, which is just extra resource the benchmark doesn&#39;t need in order to perform. One furniture
retailer in the panel makes £608k of operating profit on £11.6m of assets, and comparable
retailers make that same £608k on &lt;strong&gt;~£5m of assets&lt;/strong&gt;. Its staffing is fine and the whole excess is
sitting on the balance sheet, so that&#39;s a conversation with a finance director about capital and
nobody needs to go anywhere near the shop floor.&lt;/p&gt;
&lt;p&gt;The second half is the radial gap, which is execution, and it&#39;s the more interesting of the two from my point of view.
One residential care operator holds £14m of assets and employs 110 staff, and so do its peers
... all seventy firms in its benchmark are built the same way. It sits at the &lt;strong&gt;19th
percentile&lt;/strong&gt; with no surplus to shed anywhere, so it has the same resources as its competitors
and less profit to show for them.&lt;/p&gt;
&lt;p&gt;Whatever explains that must be in the running of the place, which means occupancy, fee mix, rostering
and the cost of covering a night shift - or whatever the constraints in care homes are. Those are scheduling and allocation problems, which are precisely what optimisation, and increasingly AI, can help with, once they are sitting inside the process. A large radial gap in an otherwise homogeneous sector is a strong signal we can see from the  outside that there&#39;s something worth improving on the inside.&lt;/p&gt;
&lt;h2&gt;What it won&#39;t tell you&lt;/h2&gt;
&lt;p&gt;DEA compares two firms standing still, so it measures the gap rather than the crossing. Some of
that gap never reaches the accounts at all ... a theatre company behind the frontier might be
badly run, or it might just be short of the one director everybody wants to work with, and
talent doesn&#39;t get tagged in iXBRL. Headroom means unrealised performance, but it doesn&#39;t mean it&#39;s
realisable.&lt;/p&gt;
&lt;p&gt;I also don&#39;t know if it works in practice, because I haven&#39;t called as bunch of high-headroom companies, persuaded them to let me do &#34;AI&#34; on them and observed the results. I guess that part will have to wait.&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>Search method shootout</title>
      <link>https://www.danhartropp.com/read/search_method_shootout.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/read/search_method_shootout.html</guid>
      <category>read</category>
      <pubDate>Mon, 10 Aug 2026 00:00:00 +0000</pubDate>
      <description>using a benchmark on a specific task</description>
      
      <content:encoded>&lt;p&gt;&lt;em&gt;Second of three posts. &lt;a href=&#34;/read/careful_design_beats_clever_tools.html&#34;&gt;Part 1&lt;/a&gt; covered building an honest benchmark honest enough to trust. This post is the reference companion: each retrieval method in turn - how it works, where it came from, what it typically scores in the literature, how I implemented it, and how it did on our data. Fair warning ... it&#39;s quite a boring post.&lt;/em&gt;  &lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;A note on the numbers: Results quoted below are MRR@10 on the benchmark set of UK Parliamentary Questions. MRR is Mean Reciprocal Rank … 0.5 means the first genuinely-correct answer typically sits around position two of the 10 items retrieved. 0.8 means it&#39;s usually right at the top. For comparison with “our results” I’m also quoting typical results for the various methods on standard datasets … MS MARCO passage dev in this case.   &lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. BM25 (keyword search)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; BM25 scores a document by how often the query&#39;s words appear in it, taking account of two extra factors: &lt;em&gt;saturation&lt;/em&gt; (the tenth occurrence of a word adds far less than the second) and &lt;em&gt;length normalisation&lt;/em&gt; (a match in a short document counts for more than one buried in a long document). &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;History:&lt;/strong&gt; Okapi BM25 came out of Robertson and Walker&#39;s work at City University London in the mid-1990s (building on the probabilistic relevance framework of the 1970s). It is still the default ranking function in Lucene, Elasticsearch and OpenSearch - which means it quietly powers a very large fraction of the world&#39;s search boxes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Typical results:&lt;/strong&gt; Around &lt;strong&gt;0.18–0.19 MRR@10&lt;/strong&gt; on MS MARCO.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation notes.&lt;/strong&gt; I used &lt;code&gt;bm25s&lt;/code&gt;, a fast pure-Python implementation (100–500× quicker than &lt;code&gt;rank_bm25&lt;/code&gt;, near-Lucene quality). It runs on CPU, the index is a few hundred megabytes, and lookups are sub-millisecond. BM25 is seriously quick, which is exactly why it&#39;s the baseline to beat.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our result:&lt;/strong&gt; &lt;strong&gt;0.56&lt;/strong&gt; &lt;strong&gt;MRR@10&lt;/strong&gt; [0.54–0.57]. Notably stronger than its MS MARCO reputation, precisely because PQ answers share wording with their questions. A genuinely strong, nearly-free baseline. This is honestly a good enough result for production use on our data, but I suspect we can do better. &lt;/p&gt;
&lt;h2&gt;2. RM3 query expansion (pseudo-relevance feedback)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How it works.&lt;/strong&gt; Run the search once, &lt;em&gt;assume&lt;/em&gt; the top handful of results are relevant, harvest the words that distinguish them from the rest, add those to the query, and search again. The idea is to bridge vocabulary gaps - if the answer says &#34;clinician&#34; and you asked about &#34;doctors&#34;, expansion can pull it in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;History.&lt;/strong&gt; Relevance models are Lavrenko and Croft (2001); RM3 is the widely-used interpolated variant. Pseudo-relevance feedback is one of the oldest tricks in information retrieval and adds a lot for classic news/legal collections.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Typical results.&lt;/strong&gt; While it &lt;em&gt;helps&lt;/em&gt; on those older collections, it is well documented to &lt;em&gt;hurt&lt;/em&gt; on MS MARCO passage, where judgements are sparse and a single wrong assumption in the first pass drags the expanded query off-topic (&#34;query drift&#34;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation notes.&lt;/strong&gt; A standard PRF pass layered on top of our BM25 index. This needs a second retrieval round, so roughly double the query cost, still CPU-only and still very quick.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our result:&lt;/strong&gt; fair &lt;strong&gt;0.50&lt;/strong&gt; [0.48–0.51] &lt;em&gt;below&lt;/em&gt; plain BM25. On this data the expansion added more drift than signal, exactly matching the MS MARCO pattern. A useful reminder that fancier is not a synonym for &#34;better&#34;. This is why we benchmark. &lt;/p&gt;
&lt;h2&gt;3. SPLADE (learned sparse retrieval)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How it works.&lt;/strong&gt; SPLADE keeps keyword search&#39;s cheap, inspectable inverted index but replaces hand-tuned term statistics with a language model. A model predicts, for every word in its vocabulary, how strongly a document is &#34;about&#34; that word — &lt;em&gt;including words the document never uses&lt;/em&gt; (so a passage about pensions can score well for &#34;retirement&#34; without actually containing that word). The result is a sparse, weighted, expanded bag of words that can be searched very efficiently. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;History.&lt;/strong&gt; SPLADE is Formal, Piwowarski and Clinchant at Naver Labs Europe (2021), refined through SPLADE++ to &lt;strong&gt;SPLADE-v3&lt;/strong&gt; (2024).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Typical results.&lt;/strong&gt; SPLADE-v3 reaches &lt;strong&gt;0.42 MRR@10&lt;/strong&gt; on MS MARCO. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation notes.&lt;/strong&gt; I used the &lt;code&gt;naver/splade-v3&lt;/code&gt; model from Huggingface. SPLADE is (should be) the best of both worlds … encoding is compute-intensive and happens on GPU; but search is sparse dot-product on CPU. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our result:&lt;/strong&gt; &lt;strong&gt;0.58&lt;/strong&gt; &lt;strong&gt;MRR@10&lt;/strong&gt; [0.57–0.60] — statistically tied with BM25 (the intervals overlap). SPLADE&#39;s learned expansion is its edge on MS MARCO, but on a corpus where questions already echo their answers, BM25 was getting most of that benefit for free, so the expansion had little left to add. At this point I was starting to question whether this was going anywhere. &lt;/p&gt;
&lt;h2&gt;4. Dense semantic search (off the shelf model)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How it works.&lt;/strong&gt; A bi-encoder embeds the question and every candidate answer into the same high-dimensional vector space, trained so that a question lands near its answer. Retrieval is then nearest-neighbour search by cosine similarity, so a question and its answer can match on &lt;em&gt;meaning&lt;/em&gt; even with no shared words at all. This is what most people today mean by search using &#34;AI embeddings&#34;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;History.&lt;/strong&gt; DPR (Karpukhin et al., 2020) made dense retrieval competitive; contrastive pre-training then made it general-purpose: Contriever (Izacard et al., 2021), E5 (2022), BGE (2023), and the current crop of instruction-tuned embedding models.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Typical results.&lt;/strong&gt; Strong bi-encoders land around &lt;strong&gt;0.33–0.40 MRR@10&lt;/strong&gt; on MS MARCO&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation notes.&lt;/strong&gt; I used &lt;code&gt;jina-embeddings-v5-text-small&lt;/code&gt; — a 0.6B parameter model on a Qwen3 backbone with a retrieval adapter. This was the strongest sub-1B open-weight model on the MTEB multilingual leaderboard at the time, which keeps the fine-tune (next section) affordable. Using embeddings at scale needs an index - I used &lt;strong&gt;FAISS flat (exact) search&lt;/strong&gt; rather than an approximate index. This isn’t necessarily needed for production, but gets rid of a layer of noise in the results. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our result:&lt;/strong&gt; &lt;strong&gt;0.74&lt;/strong&gt; &lt;strong&gt;MRR@10&lt;/strong&gt; [0.72–0.75]. Boom. A clear step above every lexical method, even untuned. And because it was being tested on questions published &lt;em&gt;after&lt;/em&gt; the model&#39;s training cut-off (Part 1&#39;s memorisation control), we can be confident that advantage comes from skill, not regurgitated training data.&lt;/p&gt;
&lt;h2&gt;5. Dense semantic search (fine-tuned model)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How it works.&lt;/strong&gt; Exactly the model above, given a training pass on &lt;em&gt;our&lt;/em&gt; question→answer pairs so the vector space is shaped to this domain. I used use a contrastive objective (&lt;code&gt;MultipleNegativesRankingLoss&lt;/code&gt;: pull each question toward its answer, push it away from every other answer in the batch) with specifically chosen hard negatives.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;History.&lt;/strong&gt; In-domain fine-tuning of bi-encoders is standard practice and consistently the highest-return step available — yet it&#39;s the one teams most often skip in favour of reaching for a flashier method or getting an existing model into production quickly. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Typical results.&lt;/strong&gt; Fine-tuning a decent base on in-domain pairs commonly adds &lt;strong&gt;0.05–0.15 MRR&lt;/strong&gt; over off-the-shelf, depending on how far the domain sits from the model&#39;s pre-training.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation notes.&lt;/strong&gt; a LoRA adapter trained on the frozen backbone, on a rented A100 GPU for a few hours. Crucially the fine-tuning used the &lt;em&gt;train&lt;/em&gt; split only and the evaluation was done on the untouched later slice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our result:&lt;/strong&gt; &lt;strong&gt;0.81 MRR@10&lt;/strong&gt; [0.80–0.83]. +0.07 over the same model off-the-shelf. A decent jump for a cost you basically only have to pay once when you fine-tune the model. After that it’s the same in production as using the off the shelf version. &lt;/p&gt;
&lt;h2&gt;6. Hybrid (reciprocal rank fusion)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How it works.&lt;/strong&gt; Run two retrievers (here: fine-tuned dense and BM25) and merge their ranked lists by Reciprocal Rank Fusion - each document scores the sum of 1/(k + its rank) across the lists, so a document ranked highly by &lt;em&gt;either&lt;/em&gt; method floats up. The theory: lexical and semantic search fail on different questions, so their union should be stronger than either.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;History.&lt;/strong&gt; RRF is Cormack, Clarke and Büttcher (2009). It&#39;s everywhere in production because it&#39;s parameter-light, needs no score calibration between systems, and rarely makes things worse.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Typical results.&lt;/strong&gt; Fusing complementary rankers usually buys a small, consistent gain — and is often the easy last few points in a competition system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation notes.&lt;/strong&gt; Weighted RRF, with the dense/BM25 weight swept on the dev slice and then applied to test exactly once. In production running this costs the sum of both pipelines plus a trivial fusion step.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our result:&lt;/strong&gt; &lt;strong&gt;0.82&lt;/strong&gt; &lt;strong&gt;MRR@10&lt;/strong&gt; [0.81–0.84].  Statistically indistinguishable from fine-tuned dense alone. On this data, once the embedding model was tuned it had already captured what BM25 would have contributed, so the hybrid added nothing measurable. Hybrids often earn their keep; here the fine-tune got there first. Worth noting that in some production use-cases the keyword bit carries more weight: if you’re searching for a person’s name, for instance. &lt;/p&gt;
&lt;h2&gt;7. A frontier API model: (Gemini)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How it works.&lt;/strong&gt; Instead of hosting an embedding model, you call one. Google&#39;s &lt;code&gt;gemini-embedding-001&lt;/code&gt; is a frontier-scale embedding model sitting behind an API: send text, get a 3072-dimensional vector back. It sits at or near the top of the public MTEB leaderboard, and it uses &lt;strong&gt;Matryoshka&lt;/strong&gt; dimensions, which means you can truncate the 3072-vector to 1024 (or 768) with almost no quality loss (I measured 0.798 at 1024 vs 0.802 at full width), so it drops straight into a 1024-dim index.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;History.&lt;/strong&gt; The lineage runs from Google&#39;s &lt;code&gt;text-embedding&lt;/code&gt; models to the Gemini-family embeddings (generally available in early 2026). Matryoshka Representation Learning (Kusupati et al., 2022) is the trick that lets one model serve many dimensionalities from a single vector.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Typical results.&lt;/strong&gt; Frontier API embedders (Gemini, OpenAI &lt;code&gt;text-embedding-3-large&lt;/code&gt;, Voyage, Cohere) top the public leaderboards, above every open sub-1B model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation notes.&lt;/strong&gt; I called it on Vertex AI, batch endpoint, at &lt;strong&gt;$0.075 per million tokens&lt;/strong&gt; (batch). Encoding the entire benchmark (271k documents plus the query sets, ~50M tokens) cost &lt;strong&gt;under $4&lt;/strong&gt;. Two things you &lt;em&gt;cannot&lt;/em&gt; do, though, and both matter: you cannot &lt;strong&gt;fine-tune&lt;/strong&gt; it (it&#39;s a black box), and every document you encode &lt;strong&gt;leaves your infrastructure&lt;/strong&gt; for a US API.&lt;/p&gt;
&lt;p&gt;The fine-tuning thing took me down a bit of a dead end. Since the Gemini model is frozen and closed, one option is a small &lt;strong&gt;projection head&lt;/strong&gt;: take Gemini&#39;s 3072-vector and train a little network on our question→answer pairs to reshape it toward our domain. It &lt;strong&gt;did nothing&lt;/strong&gt; - 0.803 with the head versus 0.802 raw. But this could be an interesting technique for “translating” between embedding models … project them all into the same embedding space using a trained adapter and you can swap the backend model. Normally you’re stuck with the one you started with, which creates risk if the model is discontinued etc. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our result:&lt;/strong&gt; &lt;strong&gt;0.80&lt;/strong&gt; &lt;strong&gt;MRR@10&lt;/strong&gt; [0.79–0.81]. Read that against the fine-tuned jina model&#39;s 0.81 [0.80–0.83]: the confidence intervals overlap almost entirely. &lt;strong&gt;A frontier general model, out of the box, matches our domain-fine-tuned specialist.&lt;/strong&gt; Depending on where you sit that&#39;s either deflating or liberating.&lt;/p&gt;
&lt;h2&gt;The scoreboard&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&#34;text-align: left;&#34;&gt;Method&lt;/th&gt;
&lt;th style=&#34;text-align: left;&#34;&gt;Fair MRR@10&lt;/th&gt;
&lt;th style=&#34;text-align: left;&#34;&gt;95% CI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;Hybrid (tuned dense + BM25)&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;&lt;strong&gt;0.82&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;[0.81–0.84]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;Dense, fine-tuned (jina)&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;&lt;strong&gt;0.81&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;[0.80–0.83]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;&lt;strong&gt;Frontier API (Gemini)&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;&lt;strong&gt;0.80&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;[0.79–0.81]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;Dense, off-the-shelf&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;0.74&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;[0.72–0.75]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;SPLADE (learned sparse)&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;0.58&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;[0.57–0.60]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;BM25 (keyword)&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;0.56&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;[0.54–0.57]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;RM3 (query expansion)&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;0.50&lt;/td&gt;
&lt;td style=&#34;text-align: left;&#34;&gt;[0.48–0.51]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So, we have two methods that are basically tied at ~0.8 (ignoring hybrid, which added nothing over fine-tuned). Which one to pick depends on what the production workload looks like. If you’ve got enough documents coming in through the day to keep a small GPU spinning then the fine-tuned local model works out pretty cheap and you get to own the model itself with no upstream dependencies … but you do have to manage infrastructure. If your workload is lumpy, or you just want an easy life, then the API route is likely to be the clear winner.&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>Careful design beats clever tools</title>
      <link>https://www.danhartropp.com/read/careful_design_beats_clever_tools.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/read/careful_design_beats_clever_tools.html</guid>
      <category>read</category>
      <pubDate>Sat, 01 Aug 2026 00:00:00 +0000</pubDate>
      <description>building a benchmark you can actually trust</description>
      
      <content:encoded>&lt;p&gt;&lt;em&gt;This is the first in a series of posts about improving search results using a real-world scenario. Inevitably, as it’s written in 2026, it’s also a bit about the process of using LLMs. This part covers the work that underpins the rest - getting a solid benchmark. Later parts will include a comparison of various information retrieval methods and how to verify the system remains accurate in production.&lt;/em&gt; &lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Let’s start with a little background. This series is about implementing a really good (and really practical) information retrieval system from the ground up. I’ve based it in the context of the political monitoring industry, because that’s what I know best. In the monitoring world, really good search (retrieval) basically &lt;strong&gt;is&lt;/strong&gt; the product, so it literally pays to be good at it. &lt;/p&gt;
&lt;p&gt;There are lots of ways to search for and retrieve information, so how can we know which among them is really good? One approach is vibe checking - just look at some results and get a sense of how good they are. In a way this is the only real test, because ultimately it’s the user’s sense of what’s good that matters. But this approach is hard to optimise against and it doesn’t scale well. So we need a benchmark. &lt;/p&gt;
&lt;p&gt;This post is all about constructing that benchmark. There are a lot of posts online that put search methods head to head to see which is the best (spoiler alert: it’s usually a hybrid approach), but they tend to use established datasets, which mean they have a pre-ordained “right” answer. That’s not an option in the real world on a greenfield project, so we’re going to spend some time (much more time than I expected when I started this) creating a benchmark we can use to judge how effective various retrieval methods are. &lt;/p&gt;
&lt;p&gt;For those who just want the headline: even with a large, clean, public dataset that was almost ideal for the task, getting to a trustworthy scoreboard cost me &lt;strong&gt;about £50 of cloud compute, several rounds of human grading over a few days, and a dozen analytical reruns&lt;/strong&gt;. Modern agentic AI made all of this dramatically faster and easier to build. When I started this project, I did wonder whether a frontier LLM would be a good enough judge to replace the benchmark stage altogether. It was not. If anything, using agentic LLMs made having a reliable, domain-relevant, benchmark even more important. &lt;/p&gt;
&lt;h2&gt;The test bed&lt;/h2&gt;
&lt;p&gt;The UK Parliament has the concept of Parliamentary Questions: MPs ask government ministers questions and the official answers are published. It&#39;s a public dataset, it&#39;s large (hundreds of thousands of question-answer pairs), and it comes in three flavours that conveniently span a difficulty range: written questions (formal, often sharing wording with their answers), oral questions (scripted questions and answers that are read aloud), and supplementary questions (the conversational and more ad-hoc follow-ups, where question and answer can look nothing alike). At a high level, our task is to search a large pool of possible answers and find the one that was actually used for a given question.&lt;/p&gt;
&lt;h2&gt;The test set&lt;/h2&gt;
&lt;p&gt;There are some general [anti]patterns that apply to experiments in general and machine learning/information-retrieval projects in particular … &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Leakage.&lt;/strong&gt; Official answers tend to restate the question, which makes plain keyword search look really good … but is it &lt;em&gt;finding the answer&lt;/em&gt; or just &lt;em&gt;echoing the question&#39;s words back&lt;/em&gt;? This is usually seen as a type of overfitting, where the technique is designed (or trained) to work really well on a specific set of data, but it’s really using shortcuts within that data to boost performance. But in our particular case, I’m calling this a feature not a bug. Some of the strongest clues about which answer goes with which question come from matching specific terms between the two. In many cases the name of the constituency is the &lt;em&gt;only&lt;/em&gt; thing that changes between otherwise identical answers.  &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tuning on the test.&lt;/strong&gt; A common silent mistake: adjust settings while watching the test score and you&#39;re no longer measuring quality, you&#39;re fitting to the test&#39;s quirks. The more complex a method, the more knobs it will have you can adjust and the more it will benefit from being tuned to the test set. The solution is simple (but quite hard to be disciplined about): only ever optimise on the training set, then use the test set to see whether it generalises.  In this case, I also split the training and test sets by time: an earlier slice to tune on, a later slice to test on, after every setting was frozen. This mimics what happens in real life. You develop on yesterday’s data, in production you see tomorrow’s (and you hope they are similar).  &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Memorisation.&lt;/strong&gt; Modern embedding models are trained on huge amounts of the public web - almost certainly including our publicly available source. This means the model itself may have already seen our test set date and would perform well until we started using data it hadn’t seen. To avoid this, I chose a test that was not only strictly after the cutoff for our training set, but also after the training cutoff date for the base model itself.  I also cross-checked results with keyword methods, retraining from scratch each time to avoid pollution.&lt;/p&gt;
&lt;p&gt;The &#34;&lt;strong&gt;right answer&lt;/strong&gt;&#34;. The nice thing about this dataset (and the main reason I chose it) is that each question has exactly one official answer that we’re trying to find. But in a haystack of hundreds of thousands of answers, there may be other documents that genuinely answer the question too and a search method should not be punished for finding those answers. Which means I needed a way to find those answers too, so I could judge the method properly. The difficulty here is that finding those answers is exactly the problem we’re trying to solve in the first place. Getting around that turned out to be (by far) the most time consuming part of setting up the benchmark. &lt;/p&gt;
&lt;h2&gt;What is the right answer anyway?&lt;/h2&gt;
&lt;p&gt;To know whether the retrieved result &lt;em&gt;genuinely&lt;/em&gt; answers a question, someone has to look at it and make an assessment. I didn’t love the idea of hand-verifying thousands of examples from the test set and these days using an LLM as a judge is often seen as a valid solution. It’s very easy to just ask the LLM “Is this a valid answer to the following question …” and accept the result. I’ve seen people do it. But I’m trying to keep at least a thin veneer of scientific method to all this, so I did a quick test. The result was disastrous and I disagreed with the LLM’s judgment about half the time. But the nice thing about LLMs is that they can be calibrated.  &lt;/p&gt;
&lt;p&gt;The solution was to get a long list of possible answers for a selection of questions and hand-grade them myself. At the risk of skipping ahead to part 2, running all of the retrieval methods across the test set generated a set of answers which were a) not the official “correct” answer but b) were selected by at least two different methods. I hand-graded 300 of these, which was a boring and tedious process but much less boring and tedious than doing tens of thousands would have been. A few rounds of trying different models and prompt-tweaking and the LLM was agreeing with me about 80% of the time. Which perhaps doesn’t seem great but … &lt;/p&gt;
&lt;p&gt;I also re-checked 100 of the answers a few days later and found that I only agreed with myself about 80% of the time too. It turns out there are loads of answers that are basically a coin-flip as to whether they answer the question or not. In effect, this means the LLM agrees with me as often as I agree with myself, which is as good a result as we’re ever going to get. The LLM then processed 76,000 judgements for about the price of a London takeaway and now I’ve got a large and validated test set that I can use to judge the performance of retrieval techniques. &lt;/p&gt;
&lt;p&gt;There are two key points here. First, &lt;strong&gt;an AI judge is a force multiplier, not an oracle&lt;/strong&gt;. It’s good at heavy lifting, but you have to keep a close eye on it and verify what it’s doing (more on that in another post). Second, &lt;strong&gt;if your benchmark comes from human judgment, it’s important to cross-check it to establish an upper limit&lt;/strong&gt; on what’s actually possible. If your benchmark comes from trusting an LLM without checking, there&#39;s a good chance you&#39;re doing it wrong. &lt;/p&gt;
&lt;h2&gt;Where the AI helped, and where a human was needed&lt;/h2&gt;
&lt;p&gt;This project was built with an agentic AI assistant doing a great deal of the hands-on work, so it&#39;s worth being precise about the division of labour, not least because I learned some lessons there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The AI was genuinely strong at:&lt;/strong&gt; writing and rewiring the data pipeline and evaluation harness; running dozens of analyses on request; scaling the relevance judgement once it was calibrated; and &lt;em&gt;diagnosing&lt;/em&gt; anomalies &lt;em&gt;when it was pointed at them&lt;/em&gt; (&#34;this number looks wrong, find out why&#34;). Ultimately, it compressed weeks of coding into days and much of that was done in the background while I got on with other things. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A human was required for:&lt;/strong&gt; the ground-truth grading (and re-grading) that set the upper limit.I was also needed to keep the LLM going when it would otherwise have stopped.Right now (2026) there are definitely practical limits on the ability of agentic LLMs to reason, particularly over longer horizons and larger problem spaces. The LLM was just as quick to decide that we’d reached a dead end and should abandon the project as it was to take a result at face value and declare success. Equally confidently in both cases. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unhelpful filters:&lt;/strong&gt; A large number (about 5% of the test set) of written questions are follow-ups of the form &lt;em&gt;&#34;pursuant to the answer of 6 January to Question 101310, was the representation written or oral?&#34;&lt;/em&gt; Claude suggested (and to be fair, my intuition agreed) that, because these reference an unseen answer, they can’t be understood in isolation and therefore should be filtered out. Claude built a filter, tested it and declared the whole thing to be a massive success. Then I actually looked at some examples. Most were perfectly understandable. The question restates its topic and the official answer addresses it, at least to some extent. I suspect this lands right at the edge of what today’s LLMs can reason about … the theory was sound but didn’t actually hold true in reality, despite the LLM convincing itself that it did.  &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude is really bad at:&lt;/strong&gt; Estimating. Many, many examples of this but the most obvious are around how much time it takes to write code. What is predicted to be “a week’s work” is often done in about 10 minutes. Some more specific examples: a batching optimisation would give a 4-7x speed-up, but when measured it actually gave none (the workload was memory-bound, not compute-bound). On one occasion Claude silently revised an estimate to match observations .. an old-school word-vector baseline was scoring suspiciously low and Claude was happy to report that it was in line with expectations. When prompted to explore, it was clear there was a bug in the pipeline. &lt;/p&gt;
&lt;p&gt;The main takeaway for me is to always inspect the data and never trust an assertion that matters. &lt;strong&gt;Validate everything by looking at the data yourself.&lt;/strong&gt;&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>The Grug Brain Developer</title>
      <link>https://www.danhartropp.com/code/grugbrain.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/code/grugbrain.html</guid>
      <category>code</category>
      <pubDate>Thu, 31 Oct 2024 00:00:00 +0000</pubDate>
      <description>Wisdom from grugbrain.dev</description>
      
      <content:encoded>&lt;p&gt;It&#39;s quite rare that I read something on the internet and agree with every word of it. &lt;a href=&#34;https://www.grugbrain.dev&#34;&gt;grugbrain.dev&lt;/a&gt; is an exception. The original is written in a &#34;caveman&#34; style which I quite like, but it makes the content a little harder to digest, so (with a little help from ChatGPT) I&#39;ve translated and summarised it here. If you like this version, check out the original ... which also has pictures. &lt;/p&gt;
&lt;h1&gt;The Practical Developer&#39;s Guide&lt;/h1&gt;
&lt;h3&gt;Introduction&lt;/h3&gt;
&lt;p&gt;This is a collection of practical insights on software development from a &#34;Grug Brained&#34; developer: someone who&#39;s been in the field for years, has seen a lot, and learned from mistakes (often the hard way). &lt;/p&gt;
&lt;h3&gt;Complexity: The True Enemy&lt;/h3&gt;
&lt;p&gt;The biggest challenge is complexity. You can’t always see complexity as it creeps into a codebase, but you can feel its impact as small changes break everything. Avoid complexity by simplifying wherever possible and understanding how each addition could make the system more fragile.&lt;/p&gt;
&lt;h3&gt;Saying No&lt;/h3&gt;
&lt;p&gt;The best weapon against complexity is “No.” Saying no to unnecessary features, abstractions, or time-wasting requirements is vital for keeping things simple. While saying yes might be good for your career, saying no keeps your project manageable, understandable, and useful in the long term.&lt;/p&gt;
&lt;h3&gt;When to Compromise&lt;/h3&gt;
&lt;p&gt;When “No” isn’t an option, look for the 80/20 solution: build 80% of the feature with 20% of the effort. It might not have every bell and whistle, but it’ll deliver the value and help keep complexity at bay. Managers often won’t even notice if you skip the last 20% of complexity.&lt;/p&gt;
&lt;h3&gt;Factoring Your Code&lt;/h3&gt;
&lt;p&gt;When building out a system, don’t over-design it early on. Let natural boundaries emerge as you work, where functionality can be isolated with minimal dependencies on the rest of the system. Waiting for these &#34;cut points&#34; to appear organically will save time and reduce the risk of over-abstracting.&lt;/p&gt;
&lt;h3&gt;Testing&lt;/h3&gt;
&lt;p&gt;Testing is crucial, but balance is key. Start with basic unit tests, but don’t overdo it in early stages. Focus more on integration tests as the system grows, and build a few key end-to-end tests that cover the main workflows. Avoid mock-heavy testing unless absolutely necessary.&lt;/p&gt;
&lt;h3&gt;Agile&lt;/h3&gt;
&lt;p&gt;Agile methods are fine, but don’t take them too seriously. Agile can be a useful framework, but the real value in development comes from prototyping, good tooling, and hiring talented developers. Agile isn’t a magic solution to every problem.&lt;/p&gt;
&lt;h3&gt;Refactoring&lt;/h3&gt;
&lt;p&gt;Refactoring is often necessary but should be done carefully. Small, incremental changes are safer than large, sweeping ones. Keep end-to-end tests updated to catch breaking changes early. And remember, too much abstraction can make refactoring a nightmare.&lt;/p&gt;
&lt;h3&gt;Chesterton&#39;s fence (Respecting Legacy Code)&lt;/h3&gt;
&lt;p&gt;There’s often a reason why old code is structured a certain way, even if it’s messy. Don’t rip out old code unless you fully understand its purpose. Sometimes code is ugly for a reason, and changing it can introduce unintended bugs.&lt;/p&gt;
&lt;h3&gt;Microservices&lt;/h3&gt;
&lt;p&gt;Getting the structure of code right is a hard problem. Microservices require this problem to be solved correctly and then add a network call. (Personal note: the ability to scale some services independtly of others can be very helpful, although it comes at the cost of complexity). &lt;/p&gt;
&lt;h3&gt;Tools and Debugging&lt;/h3&gt;
&lt;p&gt;Learning and mastering your development tools is a big productivity boost. IDEs, debuggers, and code completion can speed up development and help you troubleshoot faster. Take the time to understand these tools deeply - indcluding conditional breakpoints and stack travsersal. &lt;/p&gt;
&lt;h3&gt;Type Systems&lt;/h3&gt;
&lt;p&gt;Type systems are most useful for autocompleting and catching simple errors, not for building hyper-abstract models. While strong typing has its place, avoid overusing generics and complex abstractions—they can make code hard to work with and maintain.&lt;/p&gt;
&lt;h3&gt;Expression Complexity&lt;/h3&gt;
&lt;p&gt;Avoid cramming too much logic into one line. Break up complex conditions into clear, named variables to improve readability and debugging. Although it may mean more lines of code, it’s easier to understand and maintain.&lt;/p&gt;
&lt;h3&gt;DRY (Don&#39;t Repeat Yourself)&lt;/h3&gt;
&lt;p&gt;While avoiding repeated code is generally good, DRY can go too far. Sometimes, duplicating a small amount of simple code is easier to understand than creating overly complicated abstractions.&lt;/p&gt;
&lt;h3&gt;Separation of Concerns (SoC)&lt;/h3&gt;
&lt;p&gt;SoC is a popular principle, but rigidly separating concerns can sometimes create unnecessary complexity. In contrast, the Locality of Behaviour principle says that &#34;The behaviour of a unit of code should be as obvious as possible by looking only at that unit of code&#34;. This flies in the face of classical thinking, but makes code easier to read and maintain in the real world. The implementation of logic can still be abstracted away, provided the invocation of that logic is obvious. &lt;/p&gt;
&lt;h3&gt;Closures&lt;/h3&gt;
&lt;p&gt;Closures are helpful, especially for abstracting operations over collections, but they can become complex quickly. Use them sparingly, as they can lead to convoluted code that’s hard to debug.&lt;/p&gt;
&lt;h3&gt;Logging&lt;/h3&gt;
&lt;p&gt;Logging is essential, especially for production systems. Use structured logs and track request IDs for distributed systems. Well-organised logging can be a lifesaver in debugging but is often overlooked.&lt;/p&gt;
&lt;h3&gt;Concurrency&lt;/h3&gt;
&lt;p&gt;Concurrency is hard, so lean toward simpler concurrency models, like stateless request handlers or job queues, which minimize the chance of complex interactions and bugs.&lt;/p&gt;
&lt;h3&gt;Optimization&lt;/h3&gt;
&lt;p&gt;Premature optimization wastes time. Profile first: always have data to back up any optimization efforts, focusing on real bottlenecks rather than theoretical improvements.&lt;/p&gt;
&lt;h3&gt;APIs&lt;/h3&gt;
&lt;p&gt;Good APIs should be easy to understand and not require deep knowledge of the underlying implementation. Create intuitive methods for simple cases and more complex interfaces for advanced cases where they are needed.&lt;/p&gt;
&lt;h3&gt;Parsing&lt;/h3&gt;
&lt;p&gt;Recursive descent parsing is usually simpler and more maintainable than using parser generators. While it might not be the latest trend, it works well for most real-world parsing needs.&lt;/p&gt;
&lt;h3&gt;The Visitor Pattern&lt;/h3&gt;
&lt;p&gt;Avoid. &lt;/p&gt;
&lt;h3&gt;Front-End Development&lt;/h3&gt;
&lt;p&gt;Avoid unnecessarily splitting frontend and backend, especially for smaller projects. Heavy front-end frameworks can add unneeded complexity, so keep things simple with minimal JavaScript where possible.&lt;/p&gt;
&lt;h3&gt;Fads&lt;/h3&gt;
&lt;p&gt;Be cautious with trends, especially in frontend development. New tools and patterns often recycle old ideas and may add more complexity than they’re worth.&lt;/p&gt;
&lt;h3&gt;Fear of Looking Dumb (FOLD)&lt;/h3&gt;
&lt;p&gt;FOLD stops people from admitting when things are confusing or complex. Senior developers should lead by example, asking questions openly. Reducing FOLD creates a healthier team environment and better code.&lt;/p&gt;
&lt;h3&gt;Imposter Syndrome&lt;/h3&gt;
&lt;p&gt;Most developers feel imposter syndrome at some point. Remember, if you’re learning and improving, you’re on the right path. The feeling of not knowing what you’re doing is more common than you think.&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>Documentation</title>
      <link>https://www.danhartropp.com/code/documentation.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/code/documentation.html</guid>
      <category>code</category>
      <pubDate>Tue, 08 Oct 2024 00:00:00 +0000</pubDate>
      <description>Getting a great user experience from a good product</description>
      
      <content:encoded>&lt;p&gt;Documentation - so often an afterthought in the development process and so often seen as users accepting defeat. There&#39;s a lot of subjectivity in this space, not least because of the variation between products and users. Some can legitimately be expected to &lt;a href=&#34;https://en.wikipedia.org/wiki/RTFM&#34;&gt;RTFM&lt;/a&gt; before using a product - others should be able to dive and and work it out. There are also legal requirements relevant to some products. &lt;/p&gt;
&lt;p&gt;Over all, my view is that, in an ideal world ... &lt;/p&gt;
&lt;p&gt;1) high level concepts and the purpose of the product should be explained clearly up-front
2) reference material should be comprehensive, and easy to access from the product itself
3) it should be possible to get meaningful results from the product without having to read the docs at all &lt;/p&gt;
&lt;p&gt;Of course, we&#39;re not in an ideal world so I&#39;ve gathered together a few more high-level thoughts that might be useful for anyone trying to write (or re-write) technical documentation. &lt;/p&gt;
&lt;h3&gt;DDD?&lt;/h3&gt;
&lt;p&gt;We&#39;re familiar with Test Driven Development and getting the User Experience right early in a product&#39;s development ... documentation is almost certainly going to be part of that experience. Sadly, we often leave writing the docs until after the product is ready for release. After all, there&#39;s no point writing documentation for something that could change during the development process is there?&lt;/p&gt;
&lt;p&gt;But what if we flipped that on its head? If we assume that new users will need to consult the documentation early on, then why not write the docs first (or at least the outline), to make sure we&#39;re delivering an excellent user experience? Reference material and detailed screenshots can probably wait - but from a UX point of view, what do we want users to experience from their first contact with the documentation ... and how does the product need to work to deliver that? &lt;/p&gt;
&lt;h2&gt;Start with why?&lt;/h2&gt;
&lt;p&gt;A common pattern with documentation for software libraries is to include a short example on the first page of the documentation - and often on the first page of the website. It&#39;s a good example of the &#34;show, don&#39;t tell&#34; communication principle. This helps address an issue with a lot of open source software products, which do a really poor job of explaining &lt;em&gt;why&lt;/em&gt; the project exists ... what is the problem they are trying to solve? If I have to learn how to use the product before I can understand what it does (and therefore whether I need it) I&#39;m very unlikely to bother. &lt;/p&gt;
&lt;p&gt;Including a short example early on does a few different things. It gives context to a potential user. At the most basic level, if I don&#39;t recognise the kind of inputs and outputs the example is dealing with, this probably isn&#39;t something I need. It also helps me compare to existing solutions ... does it look more flexible, does it have more features, is it easier to use? Finally, it gives a sense of what using the product will be like - and will it fit with my existing code base? This last point is really important for developer experience and is most easily conveyed with a few short code examples.  &lt;/p&gt;
&lt;h2&gt;Readability and cognitive load&lt;/h2&gt;
&lt;p&gt;People generally use a product because they want the benefits that product brings. Good documentation is an important, but indirect, part of making those benefits happen - the more time a user has to spend in the documentation, the less time they are spending using the product and the longer it will take for the benefits to arrive. &lt;/p&gt;
&lt;p&gt;Make it easy to use the docs and you&#39;ll make it easier to use the product. A big part of this (as with code and really any written material) is readability, consistency and reducing cognitive load. These things all help users to navigate the documentation, find what they are looking for and understand it. My advice is to take a pretty rigid approach here - particularly in relation to cognitive load. Don&#39;t explain high level concepts in reference material (but linking to them is a great idea) and don&#39;t get bogged down in covering every possible parameter when writing a how-to guide. Users generally want to use the docs for as little time as possible before getting back to using the product ... as a documentation author, your job is to make that as easy for them as possible. &lt;/p&gt;
&lt;h2&gt;Diátaxis&lt;/h2&gt;
&lt;p&gt;If you&#39;re the sort of person who reads a lot of documentation (this applies to most programmers) then you may have noticed a trend for organising documentation into categories such as how-to guides, tutorials, concepts and reference material. This is a very popular structure and has been formalised into a framework called &lt;a href=&#34;https://diataxis.fr/&#34;&gt;Diátaxis.&lt;/a&gt; &lt;/p&gt;
&lt;p&gt;This is well worth looking into ... it&#39;s essentially a set of guidelines for writing the right kind of documentation for a particular use case. It splits concepts along two axes ... action-cognition and acquisition-application. These in turn lead to the categories listed above. Once you&#39;re familiar with the concepts, you&#39;ll start to see them in all sorts of documentation. &lt;/p&gt;
&lt;p&gt;It&#39;s an excellent idea and a very accessible website, full of useful information for anyone involved in writing technical documentation. &lt;/p&gt;
&lt;h2&gt;Self-documenting code (and products)&lt;/h2&gt;
&lt;p&gt;It is possible (in theory) to write code that is self-documenting, by using descriptive names and clear structures. The idea is that the code is so easy to follow that additional documentation is unnecessary. This works up to a point, but even very clearly written code can&#39;t explain &lt;em&gt;why&lt;/em&gt; something is being done a particular way. Comments are very useful for explaining &lt;em&gt;why&lt;/em&gt; and if they written in a structured way, can be  pulled together and formatted by scripts to make documentation. Typically this approach is used for reference material, rather than detailed conceptual guides. With automated checks in place, it becomes very easy to make sure that your code library is 100% documented. &lt;/p&gt;
&lt;p&gt;To be honest, I&#39;m not a huge fan of what tends to come out of these systems. This kind of documentation is great for code inspection tools in modern editors, which can save a lot of time having to flick back and forth between files, but it usually doesn&#39;t add much more value than that. &lt;/p&gt;
&lt;p&gt;Writing great documentation (including reference material) takes time and effort - there are no short cuts. If you&#39;ve got great reference documentation, putting the relevant sections in the right place in the code is wonderful and makes it immediately available - but you need to have great docs in the first place. Too often, what actually gets generated is a basic description of a function, listing it&#39;s parameters and return type. Using strong typing allows modern editors to do the same thing, without giving the product manager false hope that 100% documentation coverage has been achieved.  &lt;/p&gt;
&lt;h2&gt;Success criteria&lt;/h2&gt;
&lt;p&gt;As with code, there is always going to a subjective element to assessing the quality of documentation. There are some obvious metrics that can be applied, such as feature/function coverage and word count, but these don&#39;t tell us much about whether the documentation is useful. &lt;/p&gt;
&lt;p&gt;However, when docs are served over the web (which these days is pretty much all the time) it becomes possible to track their use to identify areas of the product that are under-documented, or where the content isn&#39;t clear enough. &lt;/p&gt;
&lt;p&gt;It also becomes possible to track use of the docs over time. Typically, a user will consult the documentation more when they first start using a product and less over time. Good documentation should help the user become proficient (and therefore consult the docs less frequently) quicker than bad documentation. There is also likely to be a shift over time from accessing high-level concepts to low-level reference material. If the curve looks different for specific bits of the product, it might be worth looking again at the docs. It might also be worth looking again at the product, but that&#39;s a different issue. &lt;/p&gt;
&lt;p&gt;Having success metrics in mind makes it possible to apply analytics techniques such as A/B testing to versions of the docs in order to improve them over time. The key point is to define what those metrics are before you start running experiments and before you start writing.&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>Code Review</title>
      <link>https://www.danhartropp.com/code/code_review.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/code/code_review.html</guid>
      <category>code</category>
      <pubDate>Thu, 11 Jul 2024 00:00:00 +0000</pubDate>
      <description>The single best way to write code good</description>
      
      <content:encoded>&lt;p&gt;I&#39;ll start with a definition in case there&#39;s anyone reading this who isn&#39;t familiar with professional coding practices. A code review is a bit like a peer review in academia. One or more people check code that&#39;s been written by someone else, with the aim of ensuring the code is of good quality.&lt;/p&gt;
&lt;p&gt;In theory, it&#39;s a simple enough process ... person A writes some code and sends it to person B, who reads the code and makes comments/suggestions as to how it can be improved - or asks questions if something is unclear. In practice, there&#39;s a lot of devil in the detail and a number of things need to be in place before code reviews are effective.&lt;/p&gt;
&lt;p&gt;Much of what follows has been distilled from elsewhere ... mostly &lt;a href=&#34;https://google.github.io/eng-practices/review/&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Standards and expectations&lt;/h2&gt;
&lt;p&gt;The person writing the code and the person reviewing it need to have a shared understanding of what good code looks like. A common way of achieving this is with a style guide or coding manual. This sets out the preferred way of doing things, from stylistic issues like spaces-vs-tabs through to security things such as banned packages. Good style guides evolve over time, but it&#39;s often helpful to start somewhere such as &lt;a href=&#34;https://google.github.io/styleguide/&#34;&gt;here&lt;/a&gt;. Style guides often specify that certain tools should be used, such as linters, compiler flags etc ... a common configuration for these tools is also then required.&lt;/p&gt;
&lt;p&gt;As well as code stuff, it is also helpful to have standards and expectations for things like tests (what types of test, what is the acceptable minimum level of code coverage), the size and content of pull requests (what is mandatory, what is optional) and the circumstances in which the code review process can be skipped (ideally never, but in small teams this isn&#39;t always realistic).&lt;/p&gt;
&lt;h2&gt;Approach and tone&lt;/h2&gt;
&lt;p&gt;A lot has been written about how to conduct a good code review and practices vary between organisations. In my view, it helps to remember that you are reviewing something that a real person has written - and that a real person will read your review - but &lt;strong&gt;you are reviewing the code, not the person who wrote it&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Overall, be kind and constructive. Phrasing something as a question can soften the impact considerably. Take time to explain why you think something and, if possible, refer to a common standard. If there is no agreed standard, this might be a good opportunity to update the style guide. If you&#39;re being nitpicky, then say that explicitly ... and it&#39;s OK for people to do something differently if a) their way isn&#39;t objectively worse or b) it is worse, but it makes very little difference.&lt;/p&gt;
&lt;h2&gt;Objective&lt;/h2&gt;
&lt;p&gt;The high level objective of the code review process is to make the whole codebase better over time. This should be the fundamental question each time code is reviewed ... does this code make the overall codebase better, or worse?&lt;/p&gt;
&lt;p&gt;Better is obviously a subjective term - the style guide is the ultimate arbiter. Better is also quite a woolly term ... if code runs faster but is harder to read, is it better? HINT: No, unless there&#39;s a very strong business reason that the code needs to run faster and there was no other way of doing it.&lt;/p&gt;
&lt;p&gt;Knowing that someone else is going to read the code that you&#39;re writing is a powerful motivator to write easily readable code. This benefits not only the code reviewer, but also the next person who has to work on that part of the codebase.&lt;/p&gt;
&lt;p&gt;It is also worth noting that, in order to make the codebase better, explanations need to be done &lt;em&gt;in the code&lt;/em&gt; not just in the code review notes. If something needs explaining, refactor the code or add comments if you have to - don&#39;t rely on the code review notes themselves.&lt;/p&gt;
&lt;h2&gt;What to look for&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Is the purpose of the change clear from the pull request?&lt;/li&gt;
&lt;li&gt;Can you understand what the code is doing and why it is necessary?&lt;/li&gt;
&lt;li&gt;Could the code have been written more clearly/simply/readably?&lt;/li&gt;
&lt;li&gt;Do the comments explain &lt;em&gt;why&lt;/em&gt; something is necessary?&lt;/li&gt;
&lt;li&gt;Are tests in place that cover every line of changed code?&lt;/li&gt;
&lt;li&gt;Are there any TODO, NOTE or similar comments that need to be addressed?&lt;/li&gt;
&lt;li&gt;Read every line of the change - check for typos and naming conventions.&lt;/li&gt;
&lt;li&gt;Take a step back and check things like titles (yes, I know).&lt;/li&gt;
&lt;/ol&gt;</content:encoded>
    </item>
    
    <item>
      <title>Building a team</title>
      <link>https://www.danhartropp.com/code/building_a_team.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/code/building_a_team.html</guid>
      <category>code</category>
      <pubDate>Fri, 28 Jun 2024 00:00:00 +0000</pubDate>
      <description>Hire for talent, not cost</description>
      
      <content:encoded>&lt;p&gt;A few reflections on the various ways of building development team capacity, from hiring freelancers or an agency to hiring staff. Based on my experiences working in bootstrapped startups and as a freelancer - this is not necessarily good advice for a well-funded high-growth firm!&lt;/p&gt;
&lt;h2&gt;Freelancers&lt;/h2&gt;
&lt;p&gt;It is tempting to think that you can package up your projects into neat little parcels and get people from Upwork to carry them out. In my experience, it takes much longer than you&#39;d think to create work packages like this and quite a long time to find and vet qualified candidates. After that, if they get it right first time the freelance option can be worthwhile. But in general, longer term relationships are harder to maintain. I&#39;ve never met anyone who actually enjoyed managing a remote freelance dev team. You&#39;re also left with the problem of not having continuous tech support available - and any bug fixing has to fit around the developers&#39; other commitments.&lt;/p&gt;
&lt;p&gt;The exception to this is designers. I&#39;ve had some very good experiences with freelance designers, perhaps because design work lends itself more readily to a freelance &#34;work packaged&#34; format.&lt;/p&gt;
&lt;h2&gt;Agencies&lt;/h2&gt;
&lt;p&gt;I&#39;ve personally never used an agency - by which I mean a development firm that offers a full-spectrum service from coding to tech support - they are often based in Eastern Europe. I have, however, been hired several times for one-off projects that were taking too long for an agency to complete. I suspect the business model of a lot of these outfits is to bid cheap and become indispensable for as long as possible, in the knowledge that whoever hired them probably doesn&#39;t have much in-house expertise. This is not a model that encourages them to be responsive and go the extra mile. Depending on your niche, you may come across cultural/language issues that can be frustrating. This is also a risk with hiring overseas talent directly, but I&#39;ve found the problems tend to recede the longer someone has worked for a UK company - and that&#39;s more likely to be true for people who are direct hires.&lt;/p&gt;
&lt;p&gt;I&#39;m sure there are good agencies out there so if you can get a personal recommendation then it&#39;s worth exploring ... just be aware that the cheap ones are cheap for a reason. You&#39;ll also need to factor in the overhead that comes with a remote contractual relationship. Asking an agency to switch priorities and work on an urgent bug isn&#39;t as simple as just leaning over to the next desk.&lt;/p&gt;
&lt;h2&gt;Using a recruiter&lt;/h2&gt;
&lt;p&gt;Use a recruitment agency. It will make your life &lt;strong&gt;so&lt;/strong&gt; much easier. Find one that specialises in your industry, the level of experience you&#39;re looking for and your tech stack. Their fees will be annoyingly high and yes, they mostly just DM people who fit the profile on LinkedIn but the alternative is having to wade through hundreds (yes seriously) of barely credible CVs submitted by bots in India. It&#39;s worth having a chat with a few firms when you&#39;re early in the process of making your first hire so you can find someone you want to build a long term relationship with - and who wants to work with you.&lt;/p&gt;
&lt;h2&gt;Hiring cheap&lt;/h2&gt;
&lt;p&gt;If you&#39;ve got time to invest in someone&#39;s training, then hiring an inexperienced but talented person can be an excellent move. It has certainly worked well for me in the past. The trick is to find someone with exceptional talent ... they need to become productive quickly enough to offset the cost of the initial period - when they won&#39;t be adding much value. An opportunistic approach can be sensible ... if you happen to come across someone who is keen and seems to have suitable talents, then move them into the dev team. But don&#39;t do it because you want to save money - if you&#39;re not paying market rates they will quickly work that out and leave. Make sure you have a clear career and pay progression plan - and stick to it. You&#39;ll need to carve out time in the working week for training - and make sure you&#39;ve got room in the budget for materials/courses/subscriptions.&lt;/p&gt;
&lt;p&gt;If all that seems like a pain (and it can be) then just pay the market rate for the most experienced developer you can afford. Don&#39;t try to hire experienced people cheaply - you want your team to feel valued, not commoditised. If you beat them down to less than 100% of the market rate, they&#39;ll bring less than 100% of their energy and talent ... and will probably leave or renegotiate as soon as a better offer comes along anyway.&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>Certification and data protection</title>
      <link>https://www.danhartropp.com/code/certification_and_data.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/code/certification_and_data.html</guid>
      <category>code</category>
      <pubDate>Fri, 28 Jun 2024 00:00:00 +0000</pubDate>
      <description>Tedious box ticking or vital back-covering?</description>
      
      <content:encoded>&lt;p&gt;Aside from internal documentation (which is very important) there are two main kinds of documentation that require attention from the tech lead in a startuppy environment.&lt;/p&gt;
&lt;p&gt;NOTE: This is based on UK experience ... it may not apply in other places. It is also not legal advice and you should not reply on it!&lt;/p&gt;
&lt;p&gt;The first kind of documentation is what I&#39;ll loosely call &#34;quality certification&#34;. These are only likely to be of interest to B2B firms - and even then only if you&#39;re selling to serious corporate/enterprise outfits with dedicated purchasing departments. At the top end (and less likely to be relevant for a startup) are the ISO accreditations e.g. ISO 9001 (Quality) and ISO 27001 (Data Security). There are also a bunch of certifications you can get that designed to reassure potential clients that you&#39;ll be careful with their data. These include things like &#34;Cyber Essentials&#34; (applicable for most firms) and PCI-DSS (for those handling credit card details).&lt;/p&gt;
&lt;p&gt;I&#39;m not a fintech expert, but if you&#39;re handling card payments you should work out for yourself which bits of PCI-DSS apply to you and what you should do about it - it looks like a total pain and probably something you should just outsource to &lt;a href=&#34;https://stripe.com/gb/guides/pci-compliance&#34;&gt;stripe&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The other certifications only ever seem to come up in tender/qualification documents that have been written by purchasing teams. Over the years, I&#39;ve been surprised at how often I&#39;ve been asked whether my firm has these certifications, how often I&#39;ve said &#34;no&#34; and how little anyone has cared. My strong impression is that the purchasing team want to be able to say they&#39;ve asked the question, but the budget holder doesn&#39;t really care about the answer. That said, I&#39;ve always been able to tell a compelling story about the security and resilience measures in place, even without a certificate.&lt;/p&gt;
&lt;p&gt;My personal view about these schemes is that they are based on sound principles, but the fact you&#39;ve got a certificate doesn&#39;t mean you&#39;ve applied those principles well. You may have a comprehensive risk and mitigation strategy written down and refresh it every quarter, but if you&#39;ve misjudged a risk or repeatedly fail to mitigate it, then the register isn&#39;t much use.&lt;/p&gt;
&lt;p&gt;Your mileage may vary of course ... and if having certification is important for your clients, then you&#39;ll just need to suck it up and get certified. There are lots of firms that can help with the process and provide templates to make it all easier.&lt;/p&gt;
&lt;p&gt;Which brings us on to data security and privacy. This is a trickier area, not least because it concerns the law, as opposed to contractual requirements (although contracts in this space are important ... see below). Data security and privacy are worth taking seriously for their own sake ... careless handling of sensitive information has the potential to make life very unpleasant for some people and I don&#39;t think that&#39;s a responsibility that should be taken lightly.&lt;/p&gt;
&lt;p&gt;It is almost certainly prudent to get some legal advice in this area - and get contracts and T&amp;amp;C documents drafted by a professional &lt;em&gt;who understands your business&lt;/em&gt;. But if something goes wrong, having a contract that&#39;s been written by a lawyer isn&#39;t a defence in itself - what that contract says will become vitally important. It therefore pays to understand this topic yourself, at least at a high level. The good news is if you&#39;re in the UK is that there is some really good (if lengthy) guidance on the &lt;a href=&#34;https://ico.org.uk/for-organisations/advice-for-small-organisations/&#34;&gt;ICO website&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What it basically boils down to is this ... you need to know what data you&#39;re holding, why you&#39;re holding it, what you plan to do with it and who is responsible for it. You then need to make sure you&#39;ve got a lawful basis for holding it (i.e. permission) and that you&#39;ve taken sensible precautions to keep it secure. You need to be particularly careful if you are holding (anywhere on systems under your control) data about people&#39;s race, religion, political opinions, trade union membership, health, biometrics, sex life or sexual orientation. See the relevant bits of the ICO website for more information. There are separate requirements that relate to marketing activity, particularly around direct marketing aimed at the public. Anyone that you are holding information about has a right to see that information - and there is a requirement for most organisations to register with the ICO.&lt;/p&gt;
&lt;p&gt;The requirements are also a bit different depending on whether you are a &#34;data controller&#34; or a &#34;data processor&#34; - the former is responsible for how the data is used, the latter just processes it under instruction. There&#39;s a good chance your firm will be both - a controller of your own information and a processor of your clients&#39; information. If that&#39;s the case, then make sure your contract is clear about who is responsible for what - ideally make them responsible for their own information as far as possible. Again, this is particularly important if clients are storing sensitive personal information on your systems - and if you&#39;re not set up to secure that kind of data, make sure the contract is clear that they shouldn&#39;t store it on your systems!&lt;/p&gt;
&lt;p&gt;This can all get quite complicated quite quickly, but a good starting point is the &lt;a href=&#34;https://ico.org.uk/for-organisations/advice-for-small-organisations/checklists/data-protection-self-assessment/&#34;&gt;ICO checklist for small organisations&lt;/a&gt;. Start there and then speak to a lawyer once you&#39;ve worked out the sort of questions you want them to answer.&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>Customers and requirements</title>
      <link>https://www.danhartropp.com/code/customers_and_requirements.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/code/customers_and_requirements.html</guid>
      <category>code</category>
      <pubDate>Fri, 28 Jun 2024 00:00:00 +0000</pubDate>
      <description>It takes less time than you think</description>
      
      <content:encoded>&lt;p&gt;Over the years, I have become more and more convinced of the business and technical sense of co-designing new products and features with input from customers (or potential customers). Often known as design sprints, Google Ventures has published detailed guidance on how they carry out the process. It&#39;s well worth reading.&lt;/p&gt;
&lt;p&gt;Away from the heavily-funded world of silicon valley, many startups can&#39;t afford the time or money required to run a 5-day sprint (or sprints) and I often come across a general reluctance to reach out to new customers until there is a product to show.&lt;/p&gt;
&lt;p&gt;This is a mistake.&lt;/p&gt;
&lt;p&gt;In my experience, people are very happy to give up some of their work time in order to help solve a problem ... provided that problem really exists for them. A week is a big ask, but an hour or two is normally fine. If you find yourself reaching out to potential clients and none of them want to spend a couple of hours developing the solution to a problem with their peers, you should probably ask yourself whether this problem (or even the market) really exists. If you don&#39;t believe me, do a little role play in your head ... imagine someone emailed to invite you to a short, high-energy session with a group of your peers/competitors to collectively design the solution to a specific problem you all face. You&#39;d get to network and you&#39;d get to shape a solution to the problem ... that someone else would then build for you. The only reason I can think of for saying no is that the problem isn&#39;t big enough to justify the time.&lt;/p&gt;
&lt;p&gt;So what can you do in two hours?&lt;/p&gt;
&lt;p&gt;As long as the problem is well defined, you can cram whole design sprint (up to the prototyping phase) into two hours. The trick is to plan it very carefully, time everything to the minute and pump up the energy in the room. Do it person - this sort of thing works poorly in a virtual environment. A good number of attendees is between six and twelve. A schedule might look something like this (there are lots of variations depending on the problem/context) ...&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define the problem as people arrive ... get them to brainstorm the relevant pain points on sticky notes before the session even starts properly. This builds energy and it means you can be clear about what the problem is from the very start.&lt;/li&gt;
&lt;li&gt;Introduce the session for no more than 5 minutes.&lt;/li&gt;
&lt;li&gt;Using large-format posters of existing products/solutions, use sticky dots to make heat maps of what people like or dislike. Collectively agree an &#34;elevator pitch&#34; (or pitches) for what the rest of the session should focus on. (20 minutes)&lt;/li&gt;
&lt;li&gt;In smaller groups, brainstorm Insights, Questions and Ideas around the agreed pitch from the previous session. Groups feed back to each other at the end. This reduces the risk of groupthink. During the feedback session, have someone write the key ideas on sticky notes and start grouping them into themes. (20 mins)&lt;/li&gt;
&lt;li&gt;After a break, assign people to themes for a couple of rounds of crazy fours (a shorter version of crazy eights) - emphasise the goal of quantity over quality! (10 mins)&lt;/li&gt;
&lt;li&gt;Vote on everyone&#39;s crazy eights with sticky dots (5 mins)&lt;/li&gt;
&lt;li&gt;Pick a couple of solutions and story-board them in more detail for 10 minutes each.&lt;/li&gt;
&lt;li&gt;Recap to flush out any themes that have emerged unseen during the session.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With breaks, in two hours you&#39;ll have a detailed understanding of the pain points your potential users face, an understanding of why they matter and a ranked list of solutions along with sketched UI and storyboards.&lt;/p&gt;
&lt;p&gt;Take that away and produce a click-functional mockup solution within a week using something like powerpoint/figma. Best to use a designer at this stage, because people can&#39;t usually see past bad design and you want to avoid too many comments about the colour of the buttons etc. Demo the mockup to the people who came to the workshop (invite the relevant decision maker too) and ask them what they&#39;d pay for it.&lt;/p&gt;
&lt;p&gt;Assuming you can make the financials stack up, now you just have to build it, sign up the people from the workshop (who are now &lt;em&gt;very&lt;/em&gt; warm leads likely to leave stellar reviews) and market it using the information you gathered about pain points!&lt;/p&gt;
&lt;p&gt;It&#39;s also worth noting the broader marketing benefits to this approach. You are showing off your brand in the context of deep understanding of the problem and developing solutions, you are being useful to potential clients which builds brand equity and you are leaving them with the impression that you are high-energy problem solvers, thought leaders and conveners. What&#39;s not to like?&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>Plan to throw one away</title>
      <link>https://www.danhartropp.com/code/make_one_to_throw_away.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/code/make_one_to_throw_away.html</guid>
      <category>code</category>
      <pubDate>Fri, 28 Jun 2024 00:00:00 +0000</pubDate>
      <description>(You will anyway)</description>
      
      <content:encoded>&lt;p&gt;Fred Brooks, in his 1975 book &#34;The Mythical Man Month&#34; makes a lot of sensible points that are still true today (albeit some of writing is quite gendered and could do with an overhaul). On the subject of estimation, he suggests planning to throw the first version of software away ... on the grounds that you&#39;ll probably bin it anyway, so you might as well plan for it. With the caveat that the maxim applies to new or unfamiliar technology rather and routine coding work, I think this is very sensible advice.&lt;/p&gt;
&lt;p&gt;It&#39;s important to be clear about &lt;em&gt;when&lt;/em&gt; in the software life cycle this applies. By it&#39;s nature, the startup world brings a lot of uncertainty, so it&#39;s tempting to think that a product will need to completely re-engineered during it&#39;s lifetime. It is important to get a product into the hands of real customers as quickly has possible, to begin the process of feedback and iteration. But if you understand your customers reasonably well and you&#39;ve been through a process of even rudimentary co-design with them, it&#39;s unlikely that your MVP will be completely wrong and destined for the bin.&lt;/p&gt;
&lt;p&gt;In my experience, provided you&#39;ve had input from a few likely customers and you&#39;ve listened to them, the first few versions of your product are likely to be evolutions, rather than completely new.&lt;/p&gt;
&lt;p&gt;But in the very early stages of technical planning for a new project, when you&#39;re making the early architectural decisions, it&#39;s unlikely that you will know all of the questions you need to ask - let alone the answers. At this stage, it&#39;s highly likely that you will make some choices that turn out to be incompatible on a fundamental level, or that something dismissed as an edge case early on turns out to be central to the way a system needs to be structured. This is the process of discovery that Brooks was getting at ... it&#39;s only once you&#39;ve spent some time working on a new project that you understand it well enough to make the right decisions. There&#39;s no shortcut to this and there&#39;s no shame in throwing away version 0.1 to start again from scratch ... particularly if you&#39;ve allowed time in the project plan for it!&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>Refactor for simplicity</title>
      <link>https://www.danhartropp.com/code/refactor_for_simplicity.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/code/refactor_for_simplicity.html</guid>
      <category>code</category>
      <pubDate>Fri, 28 Jun 2024 00:00:00 +0000</pubDate>
      <description>By simplicity, I mostly mean readability</description>
      
      <content:encoded>&lt;p&gt;For those that aren&#39;t familiar, &lt;strong&gt;refactoring&lt;/strong&gt; is like tidying up your code. It’s the process of restructuring existing code without changing its external behaviour. The goal is to make your code simpler, cleaner, and more readable. This can have a huge impact on the maintainability and scalability of your software. Programmers (the good ones anyway) spend quite a lot of time refactoring their code ... and that&#39;s a good thing.&lt;/p&gt;
&lt;p&gt;But it&#39;s not quite that simple.&lt;/p&gt;
&lt;p&gt;There are often competing goals in play. The process of refactoring code often throws up opportunities to make it run faster - which is a good thing and can reduce running costs. But if this comes at the cost of greater complexity or reduced readability it&#39;s probably not worth doing.&lt;/p&gt;
&lt;p&gt;Let&#39;s take the example of caching. Ignoring for a moment that caching is famously one of the &lt;em&gt;hard things&lt;/em&gt; in computer science, it&#39;s a common way to speed up software, basically by reusing common bits of data rather than fetching or calculating them every time they are needed. This saves time, which means more things can be done in a given period and fewer CPU cycles are needed.&lt;/p&gt;
&lt;p&gt;However, in order to implement caching, more code needs to be written and more tests are required to make sure the code is working properly. Testing a caching mechanism can be tricky as there needs to be a way of a) making sure the data is cached properly and b) making sure the code still works when the data isn&#39;t cached. This doesn&#39;t just apply when the cache is being set up - the next developer who comes along is going to have to understand how the caching works - which is going to take some time. It&#39;s probably also a good idea to document the caching mechanism - and the problem it&#39;s trying to solve - in case a future developer gets frustrated and deletes it. It &lt;strong&gt;might&lt;/strong&gt; be worth doing this kind of refactoring if the overall cost saving from increased performance outweighs the cost of increased developer time - but it probably isn&#39;t and you should do the calculation first!&lt;/p&gt;
&lt;p&gt;The other side of this coin is refactoring for readability.&lt;/p&gt;
&lt;p&gt;Given the choice, I would much rather be presented with code that is readable and doesn&#39;t work, than working code that is hard to understand. This sometimes takes people by surprise, as they assume that the bare-minimum standard for code is that it should work. This is true, for code that makes it into production. Although even then there will always be hidden bugs and edge cases, so maybe it&#39;s more accurate to say it&#39;s &lt;em&gt;mostly&lt;/em&gt; true for production code.&lt;/p&gt;
&lt;p&gt;But before code makes it into production, at least two people will (should) work on it - the person that wrote it and the person that reviews it. Reviewing readable code can be a very quick process, but reviewing poorly written code can take a very long time. It&#39;s also quicker to find and squash bugs in code that is readable ... and this is time that really matters once the code is in production.&lt;/p&gt;
&lt;p&gt;Determining how &#34;readable&#34; code is can be something of a subjective exercise. What makes sense to me may not make sense to you. The most important thing is consistency. Things should be where I expect to find them and formatted in a way that my eye expects to see. In short, follow the style guide.&lt;/p&gt;
&lt;p&gt;You have got a style guide, haven&#39;t you?&lt;/p&gt;
&lt;p&gt;Things should be named sensibly and consistently (noting that naming things is the other &lt;em&gt;hard thing&lt;/em&gt; in computer science). They should describe the reality of their purpose, variables should be nouns and functions should be verbs. Get those things right and most code becomes self-documenting. It should always be clear from the code itself &lt;em&gt;what&lt;/em&gt; is happening and &lt;em&gt;how&lt;/em&gt; - but if it isn&#39;t clear &lt;em&gt;why&lt;/em&gt; that thing is necessary then it might be worth adding a short comment.&lt;/p&gt;
&lt;p&gt;Opinions vary about the correct level of abstraction and how much repetition is acceptable. My view is that it should be possible to understand any individual logical unit of code based only on what fits on one screen of text. Utility functions are great and can reduce complexity, but only if what they are doing is obvious from their name and there are no hidden side effects ... otherwise each one adds more cognitive load as the reader has to dig further and further into the code to understand what&#39;s happening.&lt;/p&gt;
&lt;p&gt;Certainly, typing the same thing out twice is a good indicator that you should consider an abstraction, but I don&#39;t treat it as a hard rule. Again, the most important thing is to be consistent in approach so the person who has to read the code in three months&#39; time (that person could well be you) can make sense of it quickly if they are familiar with the overall codebase.&lt;/p&gt;
&lt;p&gt;In summary, when refactoring (and you should refactor often) consider readability first, then overall simplicity, then performance if absolutely necessary.&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>Testing</title>
      <link>https://www.danhartropp.com/code/testing_and_load_testing.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/code/testing_and_load_testing.html</guid>
      <category>code</category>
      <pubDate>Fri, 28 Jun 2024 00:00:00 +0000</pubDate>
      <description>You can have too much of a good thing</description>
      
      <content:encoded>&lt;p&gt;Before any code goes into production, a set of automated tests should be written and those tests should all pass. Where appropriate (e.g. front-end changes) there should also be a manual check that things work as expected and look right on a range of screen sizes. Where code interacts with existing code or the data layer, the tests for those should also all pass. If the tests are no longer relevant, they should be rewritten.&lt;/p&gt;
&lt;p&gt;So far so simple.&lt;/p&gt;
&lt;p&gt;Having tests that pass is one thing, but knowing you&#39;ve got the &lt;em&gt;right&lt;/em&gt; tests is another. I take a somewhat pragmatic but rigid approach to this. The bare minimum level of testing as as follows ...&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Aim for at least 95% code coverage. In the real world, there are always weird edge cases that need to be accommodated and are very hard (or slow) to test reliably - it isn&#39;t always worth writing tests for these. If you can explain in a one-line comment why a specific section of code doesn&#39;t need testing, then it&#39;s OK to skip it. But make that section of code as small as possible - a couple of lines is about right.&lt;/li&gt;
&lt;li&gt;As a minimum, have test cases for a) the happy path, b) handling the most likely failure path (probably arising from dodgy user data) and c) any bugs that have arisen in production.&lt;/li&gt;
&lt;li&gt;Unit test and mock everything that it is sensible to unit test. For instance, anything in utils files and the database access layer. Run these tests whenever the relevant files are changed, for &lt;strong&gt;any&lt;/strong&gt; reason.&lt;/li&gt;
&lt;li&gt;End to end test everything. Tests should use a copy of the live database in an environment that is as close to production as possible.&lt;/li&gt;
&lt;li&gt;Have tests that enforce a data schema across the whole codebase. These don&#39;t need to hit the database directly, but should confirm strict typing ... if my code expects a field to be a datetime and yours expects a string, the test should fail. Run these tests for the whole project when the data schema changes for &lt;strong&gt;any&lt;/strong&gt; reason.&lt;/li&gt;
&lt;li&gt;Tests should be quick to run. Don&#39;t make me wait more than a minute for the whole suite to run. If it&#39;s tricky to achieve this while running end to end tests in a close-to-live environment, then refactor the code or the whole architecture to make it work.&lt;/li&gt;
&lt;li&gt;Follow a CI/CD process - but keep it as simple as possible. In a small team, a complex CI/CD process can add unhelpful overhead where a simple code review and approval process would have been fine.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The bottom line is that you&#39;re always going to find bugs in production, so there&#39;s little point in writing hundreds of tests cases for every scenario you can think of. But do test every happy-path scenario because that means you reduce the risk of putting code into production that breaks something else.&lt;/p&gt;
&lt;p&gt;A quick word about load testing ...&lt;/p&gt;
&lt;p&gt;Periodically (and whenever significant architectural changes are made) it&#39;s worth load testing a close-to-live environment to destruction. There are good automated tools for this - put some limits on your auto-scaler and find out how much traffic your system can handle - and where the bottlenecks are. This is useful for a) those times when you get a massive spike in legitimate traffic and b) estimating your future costs given assumptions about growth and traffic.&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>When things go wrong</title>
      <link>https://www.danhartropp.com/code/when_things_go_wrong.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/code/when_things_go_wrong.html</guid>
      <category>code</category>
      <pubDate>Fri, 28 Jun 2024 00:00:00 +0000</pubDate>
      <description>An opportunity to impress, or cut your losses</description>
      
      <content:encoded>&lt;p&gt;In any business, things sometimes go wrong. You should of course try to prevent this, but in the real world sh*t happens. It&#39;s often a stressful time for the technical team and for anyone in a customer facing role. It helps to remember that most people (at least in a B2B setting) have been on both sides of things going wrong and are likely to have some sympathy for you, despite expressing frustration.&lt;/p&gt;
&lt;p&gt;If your product/service isn&#39;t business-and-time-critical for your customers, they may not be all that bothered about a minor outage, even if it feels like a big deal to you. In any event, be proactive and fairly honest in your dealings with customers, including telling them once the incident has been resolved. Thank them for their patience and provide affected customers with regular updates about mitigation measures. Depending on the incident, you may also be able to use the fact that you&#39;ve got their attention to deepen your relationship with them ... we&#39;re taking the opportunity to review the code and would value their views about XYZ features etc.&lt;/p&gt;
&lt;p&gt;If you can&#39;t turn an incident into an opportunity, then make a honest assessment of how much damage has been done to your reputation in the eyes of any affected customers. Consider how important (financially or in other ways) those customers are to your business and consider putting your energy into maintaining good relationships rather than chasing after those that are irreparably damaged.&lt;/p&gt;
&lt;p&gt;Behind the scenes, it really helps to have a clear and simple process in place for dealing with incidents (and things which might be incidents, but you&#39;re not sure yet). Don&#39;t make it too long or too complicated ... just cover what the escalation mechanism is, how and when to communicate internally and how to make decisions - including deciding when and what to say externally.&lt;/p&gt;
&lt;p&gt;The best time to think about these things is before they happen ... ask yourself what information you&#39;ll need in order to make useful decisions, who should be responsible for gathering that information and who is responsible for making decisions as the incident unfolds. The second best time to think about these things is after an incident has just occurred - this is when things are fresh in your mind and you can learn useful lessons for next time.&lt;/p&gt;
&lt;p&gt;Typically, a standing plan for incident response might look something like ...&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Customer or tech team becomes aware of potential incident and notifies x,y,z&lt;/li&gt;
&lt;li&gt;Investigate for 30 mins then huddle for 5 mins&lt;/li&gt;
&lt;li&gt;Decide on a plan for further investigation / urgent mitigation / further decisions&lt;/li&gt;
&lt;li&gt;Execute the plan and adapt as necessary&lt;/li&gt;
&lt;li&gt;Wash up &amp;lt;-- THIS IS IMPORTANT&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;During the washup/lessons learned phase, think about preventing similar problems in the future, how to make incident response easier in the future, what aspects of the incident could have been managed better and whether any changes to the process should be made as a result. Be careful not to just fight yesterday&#39;s fire ... any plan needs, above all else, to be simple and flexible.&lt;/p&gt;</content:encoded>
    </item>
    
    <item>
      <title>YAGNI</title>
      <link>https://www.danhartropp.com/code/yagni.html</link>
      <guid isPermaLink="true">https://www.danhartropp.com/code/yagni.html</guid>
      <category>code</category>
      <pubDate>Fri, 28 Jun 2024 00:00:00 +0000</pubDate>
      <description>You ain&#39;t gonna need it</description>
      
      <content:encoded>&lt;p&gt;Developers like developing things. More specifically, they like developing new things. Left to their own devices, developers have a tendency to build many more things than is strictly necessary, in ways that are more complex than needed, in order to handle every conceivable use case and weird edge condition.&lt;/p&gt;
&lt;p&gt;YAGNI combats this tendency with a straightforward idea: don&#39;t add features, capabilities, or bits of code unless you absolutely need them right now. Instead of planning for every possible future scenario, focus on what&#39;s essential at the moment. This approach is a perfect fit with Agile methods, which emphasise delivering small, manageable changes rather than big, speculative ones.&lt;/p&gt;
&lt;p&gt;I&#39;m a wholehearted believer in YAGNI. It fits really well with &#34;optimising for speed to market&#34; and the idea that your first (MVP) attempt is going to need rewriting anyway. The less code you&#39;ve written in the first place, the faster it&#39;s going to be to make it work properly the second time round.&lt;/p&gt;
&lt;p&gt;YAGNI does, however, require a shift in mindset. Aside from having to watch over development teams like a hawk to make sure they aren&#39;t building things that aren&#39;t needed, there is inevitably going to be pressure from stakeholders (and therefore the customer and sales teams) pushing for features.&lt;/p&gt;
&lt;p&gt;This is tough - and sometimes impossible - to resist. It&#39;s always worth asking stakeholders how much they value something (or how quickly they really need it) by getting them to rank any requirements and then asking what would happen if they only got the top two. Chances are they&#39;ll still sign up and you&#39;ll have saved yourself a whole load of legacy code problems.&lt;/p&gt;</content:encoded>
    </item>
    
  </channel>
</rss>