More from The personal website of Matt Ström-Awn
If you work in software (as a designer, developer, PM, or any adjacent role), you’re probably aware of just how much AI has changed the way code is created. And if you’re an engineer, you’re definitely feeling the downstream impacts of AI’s speed. Way, way more code is being written. But the way we review code hasn’t changed that much. But if you’re not familiar, code review is a step in the software development process where developers look at code before it gets shipped. It’s been a cornerstone of engineering teams’ workflows for almost as long as there have been engineering teams. AI coding has turned code review into a bottleneck; a human can only look at so much code, so reviews have started taking longer, and each engineer’s review backlog is growing exponentially.1 AI code review tools have stepped into the breach, eliciting an earnest grumble from developers: AI reviewing AI-written code is like a student grading their own test. Execs don’t have much sympathy, though. We can move faster, so we should. So teams are stuck in a traffic jam, trying to figure out how to review code faster, without sacrificing quality. I’d like to argue that the source of this problem is more fundamental than speed vs. quality. We’re in this situation because somewhere along the way we swapped one question for another. Code review was always meant as a verification step: does the code produce the outcome we want? But it’s a hard question to answer, so we started asking an easier one instead: is this good code? Let’s go back to the original question. Code review should get better at verifying outcomes — faster, yes, but more accurately, and for the system as a whole, not just single lines of code. What is code worth? The premise of code review is that code can be good or bad, and that bad code can be turned into good code, and that good code is important. This is a sound premise. Bugs are obviously bad for business. But we’ve made an attribute substitution: we decide whether code looks good as a stand-in for whether the software works. Engineers use mental shortcuts to determine the goodness or badness of code. They range from very well-documented patterns (use guard clauses to return early, use pure functions where possible) to vibe-based ‘code smells’ (magic numbers, deep nesting). But these aren’t written in stone. They are debated and re-evaluated constantly. Even if there are inviolable definitions of good code, writing good code isn’t guaranteed to result in valuable software products. Sometimes, it works out that way; before Facebook bought it, WhatsApp kept its codebase lean enough that 35 engineers could serve 450 million users.2 But there are far, far more examples of software succeeding despite being written poorly: Undertale would never have passed a single code review (almost all of its dialog was one giant switch statement), but became one of the most beloved games of the past decade. Good code does not strictly equal good software. Code itself has no intrinsic value. So what are we actually reviewing when we review code? Verifying There’s just no substitute for actually running the code and verifying that it does its job well. I don’t mean unit tests, or even integration tests, but running the code in total, a full end-to-end test. Doing this at scale for large applications is difficult. At an org with hundreds of AI-enabled engineers, you likely have thousands of pull requests a day. Spinning up and breaking down complete copies of your entire application (likely, application cluster) is time- and resource-intensive. Even if you do manage it, verifying under realistic conditions may be completely infeasible (imagine simulating the activity that Stripe handles — 1.75% of global GDP). Which is to say that you won’t know if your code works or not until it’s in users’ hands. Traditional code review has two outcomes: You are confident enough that the code works well enough to ship it You are not confident enough that the code works well enough to ship it Verifying your code under the most realistic scenario possible (i.e., yolo, straight to production) has three: You are sure the code works, and it’s already shipped. Good job! You are sure the code doesn’t work, so you improve the code until it works You don’t know because the system failed in an interesting way. Instead of confidence, you have information, and can use it to improve the system. Verifying a major change by shipping it is risky, but the closer you can get to it, the more your review is actually worth. Feature flags, canary releases, observability and CI/CD — the stuff of modern devops — have all been commoditized to the point where it’s cheap to build in the safeguards that make the yolo approach tractable (even at Stripe scale). Stop reviewing code3 You’re probably doing code review for the wrong reasons. Let AI do the work of making code better: tracing code paths, finding, and fixing obvious issues. And if it’s a student grading their own test, so what? AI can make the code better. People can check if it works. Invest in the tools to verify your code in realistic conditions, deploying it directly to users if possible. Use your own eyes and brain to see it working, or see it not working. Learn from the difference, share what you learn with your team, and build feedback loops that improve the system the more it fails. And eventually the failures will become rare and incredibly interesting. Footnotes & References GitHub saw 1 billion commits in all of 2025; by April 2026 it was seeing 275 million a week. ↩︎ Cade Metz, “Why WhatsApp Only Needs 50 Engineers for Its 900M Users,” Wired, September 15, 2015. ↩︎ Of course, there are situations where safety means protecting human lives or massive economic outcomes, and reviewing code is the only safe option. The IRS shouldn’t ship updates to the code it uses to disburse tax refunds without reviewing the code. Waymo shouldn’t ship updates to its self-driving cars’ algorithms without reviewing the code. But you probably aren’t working for the IRS or Waymo. And if you are, get in touch ;) ↩︎
Four years ago, I wrote “How to pick the least wrong colors.” The gist is: picking a categorical color palette is an optimization problem. There’s no such thing as the right colors. But if you use the right cost function, and the right kind of hill climbing, you can at least get the least wrong ones. Since the original post I’ve been slowly picking away at improvements and new approaches. Now that we’re past the singularity, I’ve put a few coding robots on the job. It’s reassuring that many of my assumptions were good ones! The robots have been able to improve the code, bridging some of the gaps in my own knowledge. Today, I’m publishing an updated version of the algorithm as an npm package, along with a fancy GUI version. While there’s still more to do, I’m proud of how far I’ve been able to take it. What’s new New evaluators More controls The public API and a CLI What’s improved The annealing algorithm Configurable color space and distance metric The results One more thing Acknowledgements What’s new New evaluators Almost as soon as I published the first version, I realized that the cost function lends itself really well to modularity. Beyond my initial evaluation functions, I could design new ones, and provide a framework for anyone to plug in their own. As a recap, my original criteria for good categorical colors, mapped to evaluation functions: Similarity — a way of measuring the similarity of one palette to another, useful for providing art direction and getting brand alignment Energy — the colors should be different from each other so they aren’t liable to be confused from one another Range — the differences between the colors should be consistent so unintended groupings don’t appear Color vision deficiency — simulating the colors under different types of color blindness (red-green, blue-yellow, partial to full tritanopia) Here’s the new evaluators: JND — strongly reject palettes that have two or more colors that are too similar Avoid — the mirror image of the similarity evaluation, push colors away from a user-defined set Contrast — compares colors, keeping them above the WCAG AA color contrast floor. Can be used with a background color to maintain contrast on a chart’s background Saliency — uses color naming study data to prefer colors that are easy to name Name difference — the mirror image of saliency, avoiding colors that share names Each of these evaluators can be weighted, indicating the kinds of tradeoffs and priorities you’d like for your color palette. Additionally, the whole evaluator system is pluggable: you can define your own evaluators and have them drive the optimizer! More controls Colors can now be fixed in place, or pinned to a particular order, making it easier to load in existing palettes and optimize all or just some of the colors. Individual channels of each color can be locked, too, meaning you can keep the saturation or hue of a color fixed while optimizing its lightness. This works in any color space. The public API and a CLI The whole package is now a proper library, with a public API. This means: 1. the whole thing is now distributable through npm, with proper versioning, 2. there’s a CLI, making it much more ergonomic for both humans and agents. The API allows for full configuration of the algorithm, as well as loading in colors to optimize. Output can be in raw color values, CSS properties, or DTCG JSON. There’s also a new reportJndIssues endpoint that allows you to evaluate palettes without optimizing them, which is useful to compare a generated palette to commonly-used ones (like Observable, d3, IBM Carbon, and more). What’s improved The annealing algorithm When I wrote the initial algorithm in 2022, I had just learned about simulated annealing. I’ll be honest: I don’t know much more today than I did then. But with AI-assisted research, I was able to solve some questions I had about the initial implementation. Now, the algorithm picks the correct starting temperature based on some random initial samples. Mutation also happens in a scaled manner, so colors change less towards the end of the optimization schedule. Iterations can be capped to prevent very long runs, and the whole thing is much, much more performant. Configurable color space and distance metric The first version of the algorithm worked in RGB space. Now, it defaults to okhsl, but even this is configurable. Individual channels can be constrained to dial in the palette’s boundaries. Also, you can choose which color distance metric you’d like to use (but the library uses CIEDE2000 by default). This flexibility is powered largely by a move from chroma.js to culori. I’ve learned a ton about color spaces since 2022, so being able to mix and match color spaces with distance metrics has been extremely useful. The results The category-colors library reliably produces better results than other palette-generating tools and industry-standard color palettes. Compared to other palette-generating tools, category-colors has more control. Palettailor, for example, optimizes for pure color difference, without accounting for color vision deficiency. QualPal brings some of the optimization parameters, but doesn’t allow for steering towards or away from arbitrary colors. Scores at 8 colors ΔEMinimum ΔEworst of CVD Name differenceMinimum Uniformitylower is better category-colors 22.6 ±1.6 13.7 ±2.0 0.35 ±0.14 best in column 0.30 ±0.02 best in column QualPal 1.1.0 24.7 21.8 best in column 0.10 0.44 Palettailor 26.6 ±2.4 best in column 4.5 ±1.7 0.34 ±0.16 0.34 ±0.04 Colorgorical 15.8 ±3.3 4.1 ±1.6 0.09 ±0.06 0.42 ±0.03 All numbers are at 8 colors. Rows with ± are mean ± standard deviation over 10 palettes; rows without are deterministic and produce one palette. category-colors and Palettailor are 10 independent runs on the same seeds; Colorgorical’s row is 10 palettes from its authors’ own sampling script at equal criterion weights. QualPal was run with CVD on, matched bounds, and takes no seed. Name difference is Heer & Stone’s 1 − cosine; Colorgorical’s own interface reports a Hellinger distance instead. Shaded cells are the best value in their column. Compared to industry-standard palettes, category-colors can produce more optimal palettes, especially at high cardinality. Scores at 8 colors ΔEMinimum ΔEworst of CVD Name differenceMinimum Uniformitylower is better category-colors 22.6 ±1.6 best in column 13.7 ±2.0 best in column 0.35 ±0.14 0.30 ±0.02 best in column Okabe–Ito 21.3 8.8 0.06 0.34 Observable 10 18.4 0.6 0.40 0.34 Tableau 10 18.1 3.2 0.24 0.32 d3 category10 16.2 1.6 0.84 best in column 0.40 ColorBrewer Set3 13.7 1.9 0.16 0.32 IBM Carbon 12.8 5.0 0.11 0.34 Same run: 8 colors, 10 trials. Reference palettes are deterministic, so they're single values. Shaded cells are the best value in their column. One more thing I’ve built a UI that consumes the package and makes it easy to generate and optimize palettes. This has been the biggest request since I published the initial essay, so it’s the thing I’m excited to share. It’s ridiculously overengineered, but hey, what else are personal projects for? Acknowledgements Many measurements come from published research: Gaurav Sharma, Wencheng Wu and Edul Dalal for CIEDE2000; Gustavo Machado, Manuel Oliveira and Leandro Fernandes for the color vision deficiency simulation; Maureen Stone, Danielle Albers Szafir and Vidya Setlur for the size-dependent just-noticeable-difference result; Jeffrey Heer and Maureen Stone, whose color naming models and the c3 data from the Stanford Visualization Group power both the saliency and name-difference evaluators. Existing palettes: Masataka Okabe and Kei Ito’s Color Universal Design set; Matthew Petroff’s sequences; and Mark Harrower and Cynthia Brewer’s ColorBrewer. Other generators laid a lot of the groundwork: Kecheng Lu and colleagues (Palettailor), Connor Gramazio, David Laidlaw and Karen Schloss (Colorgorical), Johan Larsson (QualPal), and Chin Tseng, Arran Zeyu Wang, Ghulam Jilani Quadri and Danielle Albers Szafir (CatPAW). Andrew McNutt, Maureen Stone and Jeffrey Heer’s color-buddy has also been indispensable. Finally, Dan Burzo’s culori made it easy to make this library colorspace-agnostic.
Imagine you’re an architect hired to redesign the floorplan of an office. The company hiring you has grown from 10-100 employees and wants to make sure the space is easy to navigate and the common areas are in the optimal location. You ask the client for the existing floorplan, but nobody can find the original drawings. They’d be useless anyway, because as the company has grown, the employees have been given license to change the space as they see fit. Their modifications range from simple decorating to major renovations. One employee walled off an entire corner of the office for themselves and nobody has seen them in weeks. No single employee can draw the floorplan from memory. Walking the space, you discover they’ve added three sets of bathrooms — as the office got more and more byzantine, it became easier to hire a contractor to build new bathrooms than to find the existing ones. Signage is a joke, and asking for directions is useless: everyone thinks they remember where things are, but their memory is inevitably outdated or otherwise biased. This is the reality of design at high-growth startups in the AI era. Engineers can build so fast and so independently that trying to map out the product area is a lost cause. Some of the traditional tools of product design — mapping and evaluating UX from a bird’s eye view — are useless. So what do you do instead? You have to stop thinking like an architect. An architect designs buildings from the outside. They use floor plans and elevations and other schematics to paint a picture of an ideal reality. They create a ‘source of truth’ that is used to coordinate engineers and builders. This is the old world of design. In the new world, the building needs to be designed from the inside. There is no ‘source of truth’ when everything changes at every moment. You do not wait to build consensus or gain a full understanding of the office to start making changes. You buy a roll of high-viz safety tape from Home Depot and start laying it down on the floor. In some spaces, you tape the outlines of more efficient walls. You put tape across the entrance to a few dead-end hallways. You buy a ‘wet floor’ sign too and put it in front of some of the bathrooms with a post-it-note saying ‘closed for cleaning.’ The staff start subconsciously heeding the taped-in redirects. Without realizing it, they are moving through the office more efficiently, congregating in common spaces again. They’re delighted to see their old colleagues, many of whom they assumed had been laid off. One team tells the others that they bought a snack machine, and new hires explore the far reaches of the office they’d previously never have seen. As one-off spaces and facilities are abandoned, you start to knock down walls. What before would have been a code red fireable offense is now completely unnoticed. The newly-available space is used to slowly expand hallways, adding a few inches of space at each pass, staying just on the edge of notice. Everyone gets a little more vitamin D. Fewer toes are stubbed on sharp corners. Each improvement brings new problems to light: now that the main bathrooms are getting more use, we’ll need to put in changing tables for new parents (the company was started by 20-year-olds who didn’t think about that when they moved in). We need to have quiet areas to balance out the noise created by more spacious open floor plans. But each problem can be solved from inside the office, taping off areas and redirecting foot traffic, manifesting desire paths into the world. For designers, our tape is code. In the same way that the inside-out architect redirects traffic, we can make small changes to product surfaces that incrementally reveal improvements. Ship like a developer, flowing your pull requests into the CI/CD pipeline as if it were just another Typescript file. How do you design from the inside? Work in the codebase. Resist the urge to create prototyping sandboxes and demo environments. If you need someone to translate your designs into the codebase, you’re on the outside, not the inside. Create tight links between your design environment and the real product. Use real data from your own API endpoints; make sure the design tokens map 1:1 between Figma and code; work at realistic screen sizes and under realistic test conditions. Ship. Put your code up for review. LLM coding agents can turn what you’ve built into production-ready code with just a few prompts. Build rituals, don’t introduce processes. Instead of introducing a new process and asking (expecting) your collaborators to follow it, simply behave as if the process is already in place. Want your team to start using PRDs before building? Make the PRD and send it out. Want to do an implementation review? Put it on the calendar. Repeat a behavior enough times and it will become ritual. Collapse feedback loops. Don’t wait for a summary of support tickets; do support rotations, and you’ll have all the input you need to fix the real problems. Ask an LLM to write SQL queries to get the data on usage, don’t wait for a dashboard or UX whitepaper. Instrument the product yourself, then push changes that move the needle. Don’t ask for permission or seek consensus; design the product from the inside.
The information age has been defined by bandwidth. The internet is limited by how much data we can squeeze into the narrow pipes of transmission infrastructure. So we invented compression, ways of representing the same object — a website, a picture, a song, a movie — within ever smaller digital footprints. YouTube, Spotify, Instagram, and the algorithms that make them work, wouldn’t be possible without it. From the very first studies into compression (at Bell Labs in the 1940s), researchers knew they’d have to accept a tradeoff: you can achieve smaller file sizes if you’re willing to accept some loss of the original data. This seems counterproductive, since the whole idea is to reproduce the data, but scientists found ways to only discard information that is imperceptible to humans. Our ears and brains tend to ‘filter out’ quiet sounds overshadowed by loud sounds. MP3s take advantage of this blind spot by stripping out the quiet bits we wouldn’t be likely to hear anyway. Our eyes and brains focus on the contrast between bright and dark shapes, reading the broad structure of images rather than granular details or tiny color variations. The JPG algorithm compresses files by throwing away information we don’t tend to process. Movies don’t actually change that much frame-to-frame. The MPG algorithm carefully chooses key frames and saves the relative motion of each pixel, making movie files much smaller in the process. A well-designed compression algorithm keeps data perceptually identical while making files much more efficient to store and transmit. A poorly designed codec can go catastrophically wrong. In 2013, David Kriesel scanned a building floor plan on a Xerox WorkCentre and noticed that a room marked 21.11m² had become 14.13m². Xerox’s implementation of the JBIG2 compression format saves space by quilting scans together from common, repeated elements; in Kriesel’s scan, it had silently replaced the original numbers with ones from another part of the document it deemed visually similar enough. After Kriesel published, reports surfaced of the same silent substitution affecting building plans, invoices, and medical records.1 Compression always changes data permanently. Common formats (JPG, MP3, MP4) make changes slowly and gently: it usually takes hundreds of cycles of saving, sharing, and re-uploading before the tool marks, called compression artifacts, become apparent. Re-save a JPG enough times and it goes blocky and washed out; iterate an MP3 and metallic tones bleed through the music; re-upload a YouTube video a thousand times and you end up with a blobby mess over unintelligible audio. Before compression: PSNR of infinity. After 10,000 compression cycles: PSNR of 14.59. If you know what to look for and how to look for it, you can learn a lot about the path that data took to get to you. That’s because compression artifacts are in turn meta-information; you can learn something new about a document by identifying and cataloging its algorithm-induced flaws. Digital forensics uses this meta-information to explore the provenance of documents, photos and videos. Compression leaves breadcrumbs that betray whether or not a document has been edited (and often who, or what, edited it). Compression artifacts can even become an aesthetic of their own. Deep-fried memes dress images up in the aesthetics of pictures that have been shared and re-shared thousands of times. Datamoshing manipulates compression algorithms to create entirely new video aesthetics. Glitch music stretches and squashes audio files, making the tool marks of audio compression audible and even musical. Compression has spawned entire fields of art and science (and jokes) all in service of the ideal compromise between fidelity and file size. Three years ago, Ted Chiang described ChatGPT as a blurry JPEG of the web. LLMs are a lossy compression of their training data, which is itself a lossy sample of all the data available to it. But the artifacts we see in AI slop aren’t in the compression. They’re in the decompression. Every AI-generated output is an extrapolation from that blurry source, vectored toward your prompt, filling in plausible detail where the compression threw information away. The output gets inflated into blog posts and LinkedIn thoughtspam, software platforms, omnichannel advertising campaigns, and movie cameos from dead actors. Chiang compared the gaps and confabulations to compression artifacts. I think they’re expansion artifacts. What do expansion artifacts look like? LLMs produce text stuffed with hedging verbs and fuzzing adjectives (delve, intricate, tapestry, multifaceted). Their paragraphs are structured as miniature essays with setup, payoff, and a signposted takeaway (This matters because…). AI-generated code over-comments the obvious and creates error handlers for operations that can’t logically fail. Image generators have had their own tells: six-fingered hands, symmetrical-but-stylistically-objectionable jewelry, text that looks like text but only if you cross your eyes. Video models struggle with continuity. Limbs appear and disappear, objects clip through each other, and physics sometimes just switches off. Each of these artifacts is the training distribution leaking through where the model’s confidence runs thin. Like compression artifacts, they double as forensic markers. In 2024, Stanford researchers tracked AI contributions to academic writing by watching for words whose frequency spiked after ChatGPT’s release (commendable, meticulous, pivotal, showcasing, etc.). They estimated that 17.5% of recent computer science papers and 16.9% of peer review text contain AI-drafted content. Sometimes the tells are less subtle: one paper in an Elsevier journal opened with “Certainly, here is a possible introduction for your topic.”2 Expansion artifacts will become aesthetic choices, too. Shrimp Jesus is my favorite, the kind of insane imagery that only an LLM would create. Power users of AI website generators (AI-pilled designers) already know how to recognize the tool marks, if only to try to prompt them away: purple gradients are an especially common tell. But as more and more non-designers use tools like Claude Design to prompt their way to fully-functional software products, I expect to see a preference for the aesthetic convergence endemic to the current crop of AI models.3 https://x.com/adamwathan/status/1953510802159219096 Expansion artifacts get genuinely dangerous when they compound, when one AI generation becomes the input to another, and another, and another. In February, an autonomous openclaw agent published a hit piece on Scott Shambaugh, a maintainer of the popular matplotlib Python library, for rejecting its code. Benj Edwards then reported the story for Ars Technica, but used AI to help him write; unsurprisingly, his article contained hallucinated quotes. This kind of Gell-Mann Amnesia for expansion artifacts leads to runaway feedback loops: A CEO dictates a five-minute voice memo Claude expands it into a strategy doc Notion’s AI turns the strategy doc into product specs Cursor vibe-codes a prototype Devin gives feedback on the PR ChatGPT writes the launch copy Intercom’s Fin support agent fields support questions. Every stage interpolates the previous context with data pulled from the blurry JPEG of its training distribution. The real danger happens when expansion artifacts show up in the training data for the next generation of generative AI. While anomalous tokens like SolidGoldMagikarp broke early models open and spread their guts out on the operating table, new models get rigorously evaluated, making it harder to see where the errors are hiding. The long tail in the distribution (quiet voices, the weird and novel phrasings, unusual and challenging ideas) fade as each successive model converges toward a homogenized — perhaps hallucinated — center. The blurry JPEG gets blurrier every cycle, leaving more and more room for nonsensical and false material to fill the voids between the tokens. Compression made the information age possible by stripping things down to fit the pipes. Expansion made the AI age possible by blowing data back up again. Both operations leave marks; we’ve learned to spot compression artifacts, but we’ve only just begun to reckon with expansion artifacts. Until we do, there’s a lot of risk to manage. Special thanks to Josh Petersel for feedback on a draft of this essay. Footnotes & References Kriesel, David. 2013. “Xerox Scanners/Photocopiers Randomly Alter Numbers in Scanned Documents.” D. Kriesel (blog), August 2, 2013. https://www.dkriesel.com/en/blog/2013/0802_xerox-workcentres_are_switching_written_numbers_when_scanning. ↩︎ Liang, Weixin, Yaohui Zhang, Zhengxuan Wu, Haley Lepp, Wenlong Ji, Xuandong Zhao, Hancheng Cao, Sheng Liu, Siyu He, Zhi Huang, Diyi Yang, Christopher Potts, Christopher D. Manning, and James Y. Zou. 2024. “Mapping the Increasing Use of LLMs in Scientific Papers.” Preprint, submitted April 1, 2024. https://doi.org/10.48550/arXiv.2404.01268. ↩︎ Copying is the way design works. ↩︎
More in design
If you work in software (as a designer, developer, PM, or any adjacent role), you’re probably aware of just how much AI has changed the way code is created. And if you’re an engineer, you’re definitely feeling the downstream impacts of AI’s speed. Way, way more code is being written. But the way we review code hasn’t changed that much. But if you’re not familiar, code review is a step in the software development process where developers look at code before it gets shipped. It’s been a cornerstone of engineering teams’ workflows for almost as long as there have been engineering teams. AI coding has turned code review into a bottleneck; a human can only look at so much code, so reviews have started taking longer, and each engineer’s review backlog is growing exponentially.1 AI code review tools have stepped into the breach, eliciting an earnest grumble from developers: AI reviewing AI-written code is like a student grading their own test. Execs don’t have much sympathy, though. We can move faster, so we should. So teams are stuck in a traffic jam, trying to figure out how to review code faster, without sacrificing quality. I’d like to argue that the source of this problem is more fundamental than speed vs. quality. We’re in this situation because somewhere along the way we swapped one question for another. Code review was always meant as a verification step: does the code produce the outcome we want? But it’s a hard question to answer, so we started asking an easier one instead: is this good code? Let’s go back to the original question. Code review should get better at verifying outcomes — faster, yes, but more accurately, and for the system as a whole, not just single lines of code. What is code worth? The premise of code review is that code can be good or bad, and that bad code can be turned into good code, and that good code is important. This is a sound premise. Bugs are obviously bad for business. But we’ve made an attribute substitution: we decide whether code looks good as a stand-in for whether the software works. Engineers use mental shortcuts to determine the goodness or badness of code. They range from very well-documented patterns (use guard clauses to return early, use pure functions where possible) to vibe-based ‘code smells’ (magic numbers, deep nesting). But these aren’t written in stone. They are debated and re-evaluated constantly. Even if there are inviolable definitions of good code, writing good code isn’t guaranteed to result in valuable software products. Sometimes, it works out that way; before Facebook bought it, WhatsApp kept its codebase lean enough that 35 engineers could serve 450 million users.2 But there are far, far more examples of software succeeding despite being written poorly: Undertale would never have passed a single code review (almost all of its dialog was one giant switch statement), but became one of the most beloved games of the past decade. Good code does not strictly equal good software. Code itself has no intrinsic value. So what are we actually reviewing when we review code? Verifying There’s just no substitute for actually running the code and verifying that it does its job well. I don’t mean unit tests, or even integration tests, but running the code in total, a full end-to-end test. Doing this at scale for large applications is difficult. At an org with hundreds of AI-enabled engineers, you likely have thousands of pull requests a day. Spinning up and breaking down complete copies of your entire application (likely, application cluster) is time- and resource-intensive. Even if you do manage it, verifying under realistic conditions may be completely infeasible (imagine simulating the activity that Stripe handles — 1.75% of global GDP). Which is to say that you won’t know if your code works or not until it’s in users’ hands. Traditional code review has two outcomes: You are confident enough that the code works well enough to ship it You are not confident enough that the code works well enough to ship it Verifying your code under the most realistic scenario possible (i.e., yolo, straight to production) has three: You are sure the code works, and it’s already shipped. Good job! You are sure the code doesn’t work, so you improve the code until it works You don’t know because the system failed in an interesting way. Instead of confidence, you have information, and can use it to improve the system. Verifying a major change by shipping it is risky, but the closer you can get to it, the more your review is actually worth. Feature flags, canary releases, observability and CI/CD — the stuff of modern devops — have all been commoditized to the point where it’s cheap to build in the safeguards that make the yolo approach tractable (even at Stripe scale). Stop reviewing code3 You’re probably doing code review for the wrong reasons. Let AI do the work of making code better: tracing code paths, finding, and fixing obvious issues. And if it’s a student grading their own test, so what? AI can make the code better. People can check if it works. Invest in the tools to verify your code in realistic conditions, deploying it directly to users if possible. Use your own eyes and brain to see it working, or see it not working. Learn from the difference, share what you learn with your team, and build feedback loops that improve the system the more it fails. And eventually the failures will become rare and incredibly interesting. Footnotes & References GitHub saw 1 billion commits in all of 2025; by April 2026 it was seeing 275 million a week. ↩︎ Cade Metz, “Why WhatsApp Only Needs 50 Engineers for Its 900M Users,” Wired, September 15, 2015. ↩︎ Of course, there are situations where safety means protecting human lives or massive economic outcomes, and reviewing code is the only safe option. The IRS shouldn’t ship updates to the code it uses to disburse tax refunds without reviewing the code. Waymo shouldn’t ship updates to its self-driving cars’ algorithms without reviewing the code. But you probably aren’t working for the IRS or Waymo. And if you are, get in touch ;) ↩︎
If you think about it, there is *a lot* of software that hasn’t been built across hundreds of industries because our best paid designers and engineers were occupied with building social networks and ad networks. "The best minds of my generation are thinking about how to make
Weekly curated resources for designers — thinkers and makers.
“Side Two” LP in progress. At 14 songs, it is nearly complete. I expect to release it before the year’s end. The post Another Saturday, Another Song. appeared first on Jeffrey Zeldman Presents.
Asking users to watch AI carefully is doomed to failure | Adaptive authoring of content that changes depending on the reader | A cluttered experience kills user focus | To predict patients’ treatment preferences, discussing medical dilemmas beat both survey responses and the patients’ own spouses | Measure outcomes, not vanity metrics | User satisfaction depends on expectations | Spotlight the main features and keep specialized options backstage | Allow users to paste passwords | Ambient AI works in the background to proactively surface relevant information during meetings | Writers don’t know what to ask, so AI should speak first, softly | Information pollution: cheap to publish, expensive to read | Give users bigger images when they want to zoom to see details | Past work must have a name so it can be found