Skip to content

From the blog

Notes on software engineering, AI, and building products.

8 min read

Building reliable tools for LLMs

Function calling, agents, and MCP all rest on the same thing: tools an LLM can invoke. But a tool built for a non-deterministic, fallible caller needs different design than one built for code. A deep-dive on what makes a tool reliable for an LLM — naming, scope, validation, error messages it can recover from, and the safety that can't live in the model.

7 min read

The AI-native Rails stack in 2026

Put the pieces together — the no-Redis Solid stack, Kamal, pgvector, and the hard-won disciplines of building with LLMs — and a coherent picture emerges: Rails is an outstanding host for AI-native applications, precisely because it lets a small team build and run both the app and its AI features. A synthesis of where the stack stands.

6 min read

Multi-agent systems: orchestration patterns

Once single agents work, the temptation is to wire several together — a researcher, a writer, a critic — and let them collaborate. Sometimes that's powerful; often it's complexity you didn't need. A deep-dive on the orchestration patterns for multi-agent systems, when they earn their keep, and the failure modes that multiply when agents multiply.

7 min read

Spec-driven development with AI agents

When an AI agent does the implementation, the specification becomes the highest-leverage artifact in the project — a clear spec produces good code, a vague one produces a confident mess. A deep-dive on why spec-driven development has resurged in the agent era, what a good spec for an agent looks like, and how the workflow actually changes.

6 min read

Ruby 3.5 and the state of YJIT and ZJIT

Ruby's JIT story has quietly become one of the most interesting things in the language. With Ruby 3.5, YJIT is mature and a sensible default — and a new method-based JIT, ZJIT, is emerging alongside it. A year-end look at where Ruby's performance work stands and why having two JIT approaches is a healthy sign.

7 min read

Designing human-in-the-loop workflows

The most reliable AI systems in production aren't fully autonomous — they keep a human at the decision points that matter. But 'human in the loop' is easy to do badly: rubber-stamp approvals, alert fatigue, humans blamed for the model's mistakes. A deep-dive on designing the human-AI division of labour so both do what they're good at.

7 min read

pgvector in production: lessons learned

I've championed pgvector as the sensible default for vector search. After running it in production, here's what the introductory tutorials leave out: index choice and its trade-offs, the recall-versus-speed dial, keeping embeddings in sync, and the operational realities that only show up at scale.

7 min read

Guardrails and evals for production agents

Putting an agent in production is harder than a single LLM call, because an agent is a loop with a multiplied failure surface — and you have to both contain it at runtime and measure it over time. A deep-dive on the two disciplines that make production agents trustworthy: guardrails that bound what an agent can do, and evals that test a whole trajectory, not a single output.

6 min read

Fine-tuning vs RAG vs prompting: choosing in 2025

There are three ways to make an LLM do what your application needs — prompting, retrieval, and fine-tuning — and teams routinely reach for the wrong one. They solve different problems, and the costs differ enormously. A trend post on what each is actually for, and a decision order that saves you from over-engineering.

7 min read

Cost-control patterns for LLM-heavy apps

LLM calls are metered per token, so in an LLM-heavy app, cost is an engineering dimension you design for — not a bill you discover. A deep-dive on the patterns that keep it under control: model tiering, caching, token discipline, budgets, and the measurement that has to come before any of it.

6 min read

Agentic coding tools: a year-one review

A year ago, AI in the editor mostly meant autocomplete. Now we have tools that edit across files, run commands, and work through a task while you watch. After a year of using them in real work, here's an honest review: what they're genuinely good at, where they fall down, and how they change the job.

7 min read

Ruby 3.4 and the long road to frozen string literals

Ruby 3.4 takes a careful step toward a change a decade in the making: making string literals frozen by default. The transition mechanism — 'chilled' strings that warn instead of breaking — is a small masterclass in how to evolve a language without breaking the world. A look at why frozen strings, and how Ruby is getting there gently.

7 min read

Self-hosting open LLMs: Llama and friends

Open-weight models like Llama 2 and Mistral mean you can now run a capable LLM on your own infrastructure instead of calling someone's API. A look at why you'd want to — privacy, cost, control — what it actually costs you in ops, and the honest question of whether you should.

6 min read

Ruby 3.3: YJIT gets faster, and Prism rewrites the parser

Ruby 3.3's headline is a faster, more mature YJIT — but the quieter, more consequential change is Prism, a new error-tolerant parser becoming the standard. A look at what's in this year's Christmas release, what you get for free, and why the new parser matters more than it sounds.

7 min read

Function calling: letting the model use your tools

Function calling lets an LLM ask your code to run a function and use the result — turning a model that can only talk into one that can look things up and act. A deep-dive on how it actually works, why it's just structured output with a loop around it, and why you stay firmly in control of what actually runs.

7 min read

Streaming LLM responses with Turbo Streams

An LLM call takes seconds, and a user staring at a spinner feels every one of them. Streaming the tokens as they generate turns dead wait into live text — and Turbo Streams is exactly the Rails primitive for pushing that text to the page. A deep-dive on wiring slow model output to a responsive UI.

4 min read

Ruby 3.1: YJIT lands, and the new debugger

Ruby 3.1 arrives on Christmas Day with YJIT — a new JIT from Shopify that finally speeds up real Rails workloads, not just benchmarks — plus a modern debugger, better error messages, and quality-of-life syntax. A practical tour.

5 min read

SOLID principles for a trading system

SOLID can feel like abstract acronym soup until a system grows complex enough to punish you for ignoring it. A war story applying the five principles to an algorithmic trading system — where each one earned its keep, and where dogmatic application would have hurt.

5 min read

CI/CD pipelines that don't flake

A CI pipeline is only useful if you trust it — and a flaky one is worse than none, because it trains the team to ignore red. A guide to building pipelines that are fast, deterministic, and trustworthy, all the way to deploy.

5 min read

TypeScript for Rubyists

Ruby developers love dynamic typing — so why is TypeScript worth your attention? A deep-dive on gradual static typing for JavaScript, what it catches, where it helps most, and how a Rubyist should think about types on the front end.

4 min read

Ruby 2.6: the JIT preview and endless ranges

Ruby 2.6.0 ships on Christmas Day with the first step toward a JIT compiler — a big deal for Ruby's future — plus endless ranges, then as an alias for yield_self, and Bundler in the standard library. A practical tour.

5 min read

GraphQL in Ruby: schema-first APIs

GraphQL lets the client ask for exactly the data it needs, solving REST's over- and under-fetching. A deep-dive on graphql-ruby — types, queries, mutations, resolvers — the N+1 trap it creates, and an honest take on GraphQL versus REST.

5 min read

Dockerising a Rails app for dev and CI

Docker has matured from a curiosity into the default way to get a consistent environment. A guide to Dockerising a Rails app for development and CI — docker-compose for the full stack, the layer-cache discipline, and where Docker helps versus where it adds friction.

4 min read

Ruby 2.5: rescue in blocks and a faster boot

Ruby 2.5.0 lands on Christmas Day with a clutch of practical ergonomics — rescue/ensure directly in do/end blocks, yield_self, Hash#slice — plus faster startup and block-passing. A practical tour.

5 min read

Designing a checkout that converts

Checkout is where a store makes or loses the sale, and most carts are abandoned there. A deep-dive on the engineering and UX of a high-converting checkout — fewer steps, guest checkout, honest validation, and the friction that quietly costs you orders.

5 min read

ES6 for Rails developers

If your JavaScript knowledge stopped at jQuery, modern JavaScript (ES6) will feel surprisingly close to Ruby. A tour of the features that matter — let/const, arrow functions, destructuring, classes, modules, and promises — through Rubyist eyes.

4 min read

Ruby 2.4: unified Integer and faster hashes

Ruby 2.4.0 arrives on Christmas Day with a long-overdue cleanup — Fixnum and Bignum merged into one Integer — plus a faster Hash, Enumerable#sum, Regexp#match?, and binding.irb. A practical tour.

5 min read

Refactoring toward hexagonal Rails

Rails is a great delivery mechanism and a poor place to keep your business logic. A deep-dive on hexagonal architecture in Rails — ports and adapters, isolating the domain from the framework, and how far to take it before it becomes over-engineering.

4 min read

Ruby 2.2: incremental GC and symbol GC

Ruby 2.2.0 arrives on Christmas Day with two garbage-collector improvements that matter for real apps — incremental GC to cut pause times, and symbol GC to finally close a long-standing memory leak and denial-of-service vector.

6 min read

Running a Bitcoin node in Docker

A war story about packaging bitcoind in a container: the initial block download that humbles you, why the blockchain must live in a volume, locking down the RPC interface, and what a full node teaches you about long-running stateful services.