Skip to content
markopteryx
Go back

Best things I read in 2025

I have always been a voracious reader, but like many people I haven’t really sat down and read a book for a few years. However, I’ve always felt the urge to read something and to this end have constantly consumed articles, blogs, posts and really anything I could get my eyes on.I started to feel as though even though I was reading a lot, I was not remembering enough. So at the start of 2025, I decided it wasn’t enough to simply passively consume this content but I wanted to engage with it. At the same time, beyond documentation I felt as though my writing skills had not improved in a very long time. So I started to keep a list of interesting things I had read and then do an end of year review.

By the end of 2025, I had read roughly 1,200 articles and here is my attempt to distill down to the 20 or so that really resonated with me. Note, these represent things I found in 2025, and since everyone is part of the ten thousand sometimes I hope you find something interesting as well. I try to avoid offering simple summaries here of the findings and I strongly recommend reading the linked material. Finally, none of my writing contains AI changes as I want to find my own writing style, not converge to the mean defined by a model.

Software Engineering and Craft

Making Software

This website is a work of art. Here Dan attempts to explain basically everything about how computers work. If you are interested in the low-level details of how computers work then its worth a visit. The project is new and only a few chapters have been written yet and I believe some parts will be paywalled in the future but its worth bookmarking for future reference.

Experts Have It Easy

Like most people, I am an expert in some domains and a novice in others. Boyd Kane articulates what everyone knows — experts are more efficient than novices while achieving better results. He goes on to paint an interesting story about navigating through a maze in the shoes of a novice and expert respectively. Pointing out that in most new situations novices don’t reach for the right tools as they simply aren’t aware of them. When my junior engineer is trying to follow the data path through an application, I find dozens of print statements when the debugger could have solved this in less than a minute. They weren’t being stubborn or lazy—as Kane observes:

the novice didn’t even know there was a decision in the first place

This article isn’t transformative, but it reminds me to extend the same patience to others that I’d appreciate when doing something new. It’s easy to forget the confusion and uncertainty of being a novice once you’ve internalized the expert’s toolkit. The junior engineer with print statements scattered everywhere isn’t making poor choices—they’re making the best choice available to them with their current mental model. My job isn’t to judge their approach, but to help them see the decisions they didn’t know existed.

Cognitive

Complexity and code design is a fan favourite of many software engineers and the advice in this domain is old, vast and often at odds with each other. This article does a good job to give a high level overview of this topic through the lens of cognitive load. We all have an intuitive understanding of cognitive load, its that feeling in as you read someones code and feel something is wrong, this is too many moving parts, this shouldn’t be this hard. These thoughts cause us to say that this code is poorly written.

Artem here suggests some straightforward practical advice, like collapsing complex conditionals:

if val > SOME_CONSTANT // 🧠+
    && (condition2 || condition3) // 🧠+++, prev cond should be true, one of c2 or c3 has to be true
    && (condition4 && !condition5) { // 🤯, we are messed up by this point
    // ...
}

can be simplified too:

let is_valid = val > SOME_CONSTANT;
let is_allowed = condition2 || condition3;
let is_secure = condition4 && !condition5;
// 🧠, we don't need to remember the conditions, there are descriptive variables
if is_valid && is_allowed && is_secure {
    // ...
}

The part that really resonated with me however was the section on inheritance nightmares. Defined as having to navigate through the class tree many levels just to determine how to change a single variable. This really hit home as in my past I still vividly recall one of my first tasks being to change the variables on a graph and what should have been a quick task (even if I had no familiarity with the codebase) turned into a whole day affair as I have to grapple with almost the entire system to understand the data flow.

This is why I advocate for a simple procedural coding style. It is worth pointing out that writing code that imposes low cognitive load onto other engineers is not a trivial task. Everyone’s definition of hard is different and changes over time. Nonetheless, it is a skill worth the discipline to cultivate.

Thoughts After 10 Years

Simply a checklist of Chris’ opinions on software engineering, I am not immune to the power of a statement said confidently. I don’t agree with everything Chris says (sometimes things are intractably complex), the general sentiment here is worth having a look over. Pieces like this always remind me of the statement “strong opinions, loosely held” — in that over time I think software engineers should acquire strong opinions but dogmatic adherence to these opinions is where the problem starts.

Best Programmers

Matthias is probably the engineer I listen to the most, in that he hosts the fantastic Rust in Production Podcast which I am big fan off. Known for his work in the Rust space, his company Corrode always maintains a blog about Rust. I find I align strongly with most opinions Matthias holds and is someone I have learnt a lot from. Here, he discusses the general features of competent engineers and here, I want to focus on one in particular — Know Your Tools Really Well.

Git, shells, IDEs, file formats, the scope of the things a software engineer is supposed to know is essentially infinite and constantly growing. However, some things are more immutable than others and to this end I would strongly recommend mastery of a single tool — your keyboard. If you have ever watched someone use emacs or vim you will have surely seen someones hands flying across the keyboard. A lifelong goal of mine is improving my proficiency with my keyboard, one that I have slowly improving over time. I don’t think its outlandish to say we should control our keyboards the way a pianist plays the piano.

Fast

Fast is fun. Catherine is right. Things should be fast and they are not. What is there even to say here, no one would argue software should be slower and so I want to just take a moment to point out some software that is actually fast.

A related article that I suggest reading here is But what if I really want a faster horse?.

Languages and Systems

The Core of Rust

Rust is my favourite language and one I took a long time to learn for several reasons, at the time my background was in JS/Python and thus I was not exposed to many of Rust’s language features; I was trying to learn lots of other things at the same time; and finally because Rust is genuinely difficult. This article does a fantastic job explaining why. To begin writing even simple programs in Rust requires knowledge of many different concepts and this is shown in the form of an example of a simple program in Rust and JavaScript.

#!/usr/bin/env -S cargo -Zscript
---
package.edition = "2024"
[dependencies]
notify = "=8.2.0"
---
use std::path::Path;
use notify::{RecursiveMode, Watcher};
fn main() -> Result<(), notify::Error> {
    let paths = ["pages", "templates", "static"];
    let mut watcher = notify::recommended_watcher(|result: Result<notify::Event, _>| {
        if let Ok(event) = result {
            let paths: Vec<String> = event.paths
                .into_iter()
                .map(|path| path.display().to_string())
                .collect();
            println!(":{:?} {}", event.kind, paths.join(" "));
        }
    })?;
    for path in paths {
        watcher.watch(Path::new(path), RecursiveMode::Recursive)?;
    }
    loop { std::thread::park(); } // sleep forever
}

This Rust program requires understanding of enums, pattern matching, traits, the borrow checker, iterators, closures and Rust’s particular brand of error handling. The JavaScript equivalent needs far fewer concepts to achieve the same result.

const fs = require('fs');
const paths = ['pages', 'templates', 'static'];
for (let path of paths) {
  fs.watch(path, (eventType, filename) => {
    if (filename !== null) {
      console.log(`${eventType} ${filename}`)
    }
  });
}
await (new Promise(() => {})); // sleep forever

But here’s the key insight: Rust’s complexity isn’t accidental bloat. These features were designed together and interlock deliberately. Pattern matching exists because enums exist. Traits exist because Send/Sync need encoding. The borrow checker enables compile-time thread safety guarantees that Java can only document in comments. You can’t learn these features one at a time because they weren’t designed one at a time—they’re a coherent system for memory safety without garbage collection.

Also, and the reason this article was memorable to me is the #!/usr/bin/env -S cargo -Zscript at the top of the Rust example. As someone who uses uv scripts in Python I was extremely surprised to find out that Rust has support for this script syntax. If you’d like to know more you can learn about it here.

Second Error Model Convergence

Alex’s writings are consistently some of my favourites and this piece, released at the very end of the year is no exception. Here, he builds up on the work of Joe Duffy’s Error Model and states that most modern languages have converged on similar error-handling semantics. Somewhat strangely I’ve always enjoyed reasoning about all the ways code can fail and its great to see error handling get the discussion it warrants.

Modern languages like Rust, Go, Swift, and Zig share three key traits that distinguish them from the exception-based model of Java/Python/JS: errors are marked at call sites (the ? in Rust), bugs trigger panics rather than catchable exceptions, and errors are first-class values (Result<T,E>) rather than side channels

Not surprisingly then, it was natural for me to find joy in Rust’s errors-as-values construction. Having watched many people now learn Rust however; you learn that some engineers really don’t want to deal with errors and those coming from languages with unchecked exceptions can struggle with the verbosity demanded by Rust. I think a lot of this is a reflection of the style of programs typically written in something like Python vs. Rust.

Unlike higher-level languages, the Rust error handling story is still evolving and while the anyhow and thiserror crates are extremely popular they are far from all-encompassing. Recently I stumbled upon the rootcause crate, which builds rich, structured errors capturing the entire history of an execution failure.

use rootcause::prelude::*;

fn read_file(path: &str) -> Result<String, Report> {
    let content = std::fs::read_to_string(path)?;
    Ok(content)
}

fn load_config(path: &str) -> Result<String, Report> {
    let content = read_file(path)
        .context("Failed to load application configuration")?;
    Ok(content)
}

fn load_config_with_debug_info(path: &str) -> Result<String, Report> {
    let content = load_config(path)
        .attach(format!("Config path: {path}"))
        .attach("Expected format: TOML")?;
    Ok(content)
}

fn startup(config_path: &str, environment: &str) -> Result<(), Report> {
    let _config = load_config_with_debug_info(config_path)
        .context("Application startup failed")
        .attach(format!("Environment: {environment}"))?;
    Ok(())
}

Which leads to the output:

 Application startup failed
 examples/basic.rs:76:10
 Environment: production

 Failed to load application configuration
 examples/basic.rs:47:35
 Config path: /nonexistent/config.toml
 Expected format: TOML

 No such file or directory (os error 2)
 examples/basic.rs:34:19

Infrastructure and Economics

The Rot Economy

I discovered this piece on January 2nd. Though written in early 2023, it still encapsulates Zitron’s broader thesis and serves as the foundation for his 2025 work. The crux of the article is that the economy is broken, with both public markets and venture capital being decoupled with what makes business genuinely valuable and the reckless pursuit of ‘number go up’ serves only to benefit the few at the expense of the many.

Societally, we have turned our markets and businesses - private and public over to arsonists

Zitron doesn’t hedge—he calls venture capital “reckless” and growth-obsessed CEOs “arsonists”. The article resonates because the past two years of software innovation have felt hollow as we watch tech companies try to justify their obscene AI spend with features nobody asked for.

Migrating to Hetzner Cloud

Another article ostensibly from a company, this article is one I have shared more than any other this year and represents a bit of a theme in my learning in 2025. The core of the article is how this non-profit coop saved 76% of their cloud bill by moving from AWS to Hetzner. For context at this point I have been using AWS for many year to deploy various services and generally had a positive experience. I knew that certain services (RDS, NAT Gateways) were extremely expensive but just figured that was the cost of business.

The real moment of enlightenment here was using this article and the accompanying comments as a springboard to start to research this in more detail. It quickly became evident that deploying your services on bare metal was not just plausible but thanks to the work of the open source community has become dare I say, straightforward.

Hetzner Server Cost

One of the biggest takeaways I got was the increased performance possible when running on bare metal. Once you stop running everything through virtualized networks the performance of modern hardware is allowed to shine through. Running a Postgres database on a dedicated server with NVMe storage and 128GB of RAM is a completely different experience to running on a t3.medium instance with EBS storage. There is so much more to say about this topic but for now I’ll just say that if you are running any non-trivial workloads on AWS or GCP you owe it to yourself to at least investigate the alternatives. Some useful links to get you started:

Local First

Ink & Switch’s manifesto for local-first software. A bit old now but I only discovered it this year. The theme is that real-time collaboration tools should prioritize local data ownership and offline-first experiences. Exquisitely written and thoroughly researched, the Ink & Switch team show that there is a place for open research in the software space. The article goes through a variety of different kinds of applications and compares them on the metrics of performance, collaboration, privacy and user control. Disappointingly, even six years later most applications fail to improve measurably on these axes.

The key tech at the heart of local-first software are CRDTs (Conflict-free Replicated Data Types) which allow multiple users to edit the same data concurrently without conflicts. CRDT’s work by ensuring that all operations are commutative, associative, and idempotent, allowing changes to be merged in any order without conflicts. A great article talking about the practical implementation in Rust is here.

Developer Experience and Tools

You’re All Nuts

Blog posts from companies are rarely more than advertising but this article was memorable for me as being the trigger to try AI in a serious way. Prior to this I had been using LLMs in exactly the way the article warns against. Combined with the matter-of-fact style the author uses and a particularly boilerplate-heavy work week, everything aligned to justify spending some time giving agents a serious go.

I will have more to say about AI in the future, but here I’ll just say that the author wasn’t wrong and given a narrow domain and strong guidelines — the robot can be genuinely useful for certain tasks.

The Frontend Treadmill

This must have been the tenth one of these I read in 2025 and this one says the most in the fewest words. Ultimately, we all know something is kind of strange about frontend frameworks and the advice here is just generally applicable. Learn the basics, keep the abstractions low and realise no matter what choice you make, it likely won’t matter. I think at this point it was four years ago I used my first frontend “framework”, having used raw React prior to that. Since then I have tried NextJS, Astro, Tanstack and even Dioxus in the Rust world. To me at least, these are all surprisingly similar, with more in common than they have apart and part of me wishes I had spent more time learning HTML and CSS as I still rely a lot on LLMs now to write CSS.

NumPy

They say you can’t truly hate someone unless you loved them first.

The opening sentence of this wonderful piece about NumPy, the core computing library in the Python ecosystem. Almost every library that does array operations in Python uses NumPy or is inspired by it. I have used it myself more many years and generally had a fine experience. I credit this to never really having to do anything more complicated than standard undergraduate physics linear algebra. However I have not had to use it seriously for at least eight years and I think that is colouring my opinions.

The author here however is much more familiar and this article goes over the idea that NumPy’s biggest issue is its lack of clarity and that after many years of using it, writing incorrect code is still extremely easy. I actually think this is often the case with older Python libraries, especially ones based around array manipulation. The early libraries coming out around this time were really built to get the job done and were often made by people who needed them for a specific use case and no general purpose libraries.

Career and Philosophy

Thrive in Obscurity

A simple reminder to just keep going. This blog, like most, I assume no one will ever read but I am writing it for me. I want to become a stronger writer, with more focused thoughts. This short essay makes it clear this effort can take years and can only ever been rewarding if the effort itself is rewarding. The way I see it, if I spend years writing stuff no one reads, if it makes me happy what is the difference.

Nobody Cares

Few things in life are a cathartic as a good rant and here we have a good one. It’s made even better as it spends devotes a sizable portion complaining about city design (one of my personal triggers). The premise is simple, if people cared a little bit more than the world would be a better place. This logic is hard to find fault with of course and the better question is why do people not care. The author is from America and while not America or from there myself, as an external observer it seems like the average American has much bigger problems in their day to day lives to give a shit about anything other than surviving.

I am extremely fortunate to work at company that does genuinely seem to give a shit. Clients call up our CEO and we aim to have fixes within a few hours. We focus a lot on getting the numbers right when getting them wrong would be a lot easier. Here, I permit myself to name a single entity that doesn’t care — The Australian Bureau of Meteorology.

Misc

No Graphics API

The hardest read of the entire year and honestly the reason its one this list. Sebastian is without a doubt an expert in his domain, here that being graphics APIs. I think I only understood about 50% of this article and that might be generous. It gives an exhaustive overview of the current graphics APIs and argues they are not fit for purpose. The biggest issue is complexity and the time it takes to create, distribute and use APIs. The real reason I wanted to briefly discuss this article was that its rare to see such deep technical insight outside formal documentation and Sebastian has done a terrific job.

Epoch

Do I own a mechanical watch, sadly no. Have I ever expressed any interest in mechanic watches, not a single iota. Did I read every word? Yes. Part of the reason was that in the first sentence he mentions Bartosz Ciechanowski’s mechanical watch article, which significantly extended my reading time.

Interjection here, if you have not read any of Bartosz’s articles, I suggest you stop reading here and go find out that interests you. You are in for a treat.

Fredrik then takes the work of Bartosz and extends it into the real world, spending considering time and effort trying to recreate the effect in the real world. Both pieces of writing are phenomenal and the effort behind both of them must have been tremendous.

Screens in Museums

I don’t have kids but this post still interested me. When I was a young kid I was very poor and spent a considerable amount of time in libraries reading books. But, every now and then my school would take us to the local museum and I’d get to touch fossils and look at all the strange things humans had done. Here, Seth states he didn’t bring his son to the Franklin Institute in Philadelphia to look at screens. The thing I actually remember most about this article is one of the comments in the HN thread about it, here, user kleiba tells us about the insane pressure public facing endeavours (schools and museums) have to add digital something to their overall experience.

Just Good

In my adventures I stumble upon many great things but not everything is strictly fantastic prose (I sure am not), or life-altering but sometimes things are just useful and this list captures those


Share this post on: