<?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>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 part 4). 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>