What we learned shipping AI agents to production
Agents are fun in a demo and humbling in production. Here are the guardrails that made ours reliable enough to trust.
Notes on software engineering, AI, and building products.
Agents are fun in a demo and humbling in production. Here are the guardrails that made ours reliable enough to trust.
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.
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.
Pre-launch evals tell you an agent works on your test set. Production tells you whether it works on reality — and only continuous monitoring closes that gap. A deep-dive on observing agents in the wild: what to log, what to measure, how to catch silent degradation, and how production failures feed back into your evals.
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.
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.
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.
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.
When Rails 8 shipped, I was enthusiastic about the no-Redis 'Solid' stack and the one-person-framework thesis. Now I've run it in production for six months. Here's the honest retrospective: where it delivered, where the rough edges were, and whether the simplification held up under real load.
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.
Small models got good enough, and devices got powerful enough, that running AI on the user's own hardware — phone, laptop, browser — is now a real option. A trend post on what local-first AI buys you, what it costs, and the hybrid reality where the honest answer is 'some of both'.
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.
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.
Committing your app to a single LLM provider is a risk: a price change, an outage, or a better model elsewhere leaves you stuck. Treating providers as interchangeable behind a routing layer buys resilience and flexibility — but it has real costs. A deep-dive on multi-provider routing, where it pays off, and where it's premature.
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.
The prototype was magic. Then we shipped it to real users, and everything the demo had hidden came due at once — cost, latency, abuse, the API going down, and the unique problem of supporting a feature whose output you can't fully predict. A war story about the gap between an LLM demo and an LLM product.
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.
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.
Rails 8 is coming with a quietly radical idea: you might not need Redis anymore. Solid Queue and Solid Cache move background jobs and caching into your database, collapsing the standard Rails production stack. A look at what the Solid libraries do, why 'just use Postgres' keeps winning, and when the old answer still applies.
Once your semantic search outgrows a toy dataset, the question arrives: stay with pgvector in Postgres, or move to a dedicated vector database like Pinecone, Qdrant, or Weaviate? A deep-dive on what actually changes at scale, what you give up by leaving Postgres, and how to decide without over-engineering.
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.
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.
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.
You can't assert exact equality on output that's different every time — so how do you test an LLM feature at all? A deep-dive on evals: building a representative test set, scoring properties instead of strings, catching regressions, and treating evaluation as the safety net that lets you change prompts with confidence.
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.
Keyword search matches words; semantic search matches meaning. A deep-dive on embeddings and vector similarity, and how pgvector lets you do semantic search in the Postgres you already run — no separate vector database required.
Most established Rails apps carry years of jQuery, and a big-bang rewrite is a trap. A deep-dive on migrating jQuery to Stimulus incrementally — the coexistence strategy, the patterns that translate, and how to do it without stopping feature work.
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.
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.
Kubernetes is the industry-standard way to run containers — and a large operational commitment. An honest look at what it gives you, what it costs, and whether a small team building a Rails app actually needs it (usually: not yet).
You can get most of the feel of a single-page app without building one. A deep-dive on Turbolinks 5 plus Stimulus — fast navigation and dynamic interactivity over server-rendered HTML — and why this 'HTML over the wire' approach is quietly the right default for many apps.
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.
React reshaped the front end around components and one-way data flow; Redux added predictable state management. A deep-dive on both for a Rails developer — components, props and state, the Redux loop — and when Redux is overkill.
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.
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.
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.
Every public app gets hammered — credential-stuffing on login, scrapers on your catalog, brute force on your API. A deep-dive on Rack::Attack: throttling, blocklisting, and fail2ban-style tracking, applied at the right layer to keep abuse off your app.
Most slow queries are a missing index away from fast. A deep-dive on indexing Postgres properly — what to index, composite column order, partial and covering indexes, reading EXPLAIN, and finding the indexes you don't have (and removing the ones you don't use).
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.
Rails 5.2 finally puts file uploads in the framework with ActiveStorage. A tour of how it works — blobs, attachments, service backends, variants, and direct uploads — and an honest comparison with CarrierWave and Paperclip.
Partials plus helpers plus instance variables make Rails views hard to reuse and impossible to test. A deep-dive on view components in Rails with Cells — encapsulated, testable units of UI, and why the component idea is coming to the server.
GDPR becomes enforceable in May 2018, and it is not just a legal box-tick — it lands squarely on the people who build the systems. A practical guide for engineers: data inventory, consent, access and erasure, and privacy by design.
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.
You cannot fix what you cannot see. A guide to monitoring a Rails app in production — the handful of metrics that actually matter, error tracking, structured logs, and alerting on symptoms your users feel rather than noise.
Getting search results back is easy; getting the right products in the right order is the real work. A deep-dive on relevance tuning for a store — boosting by business signals, synonyms, typo handling, zero-results, and measuring whether your changes actually help.
A war story: we built fast on MongoDB, hit the limits of the document model, ran Mongo and Postgres side by side in one Rails app, and migrated collection by collection to Postgres without downtime. What it cost, how we did it, and what we'd do differently.
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.
View logic has nowhere good to live in Rails — it leaks into models and clutters templates. A deep-dive on decorators and presenters: the difference, when to use each, plain PORO vs Draper, and how to keep presentation out of your domain.
Rails 5.1 makes peace with modern JavaScript and tightens up secrets management. A tour of the release — Webpacker and yarn, encrypted secrets, system tests, form_with, and dropping jQuery as a default.
Taking money is the part of e-commerce you cannot afford to get wrong. A deep-dive on integrating a payment gateway correctly — tokenization, idempotency keys, webhooks as the source of truth, and modelling a payment as a state machine.
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.
The Rails asset pipeline was built for a simpler JavaScript world. As front ends grew into real applications with npm and modules, Webpack took over. A deep-dive on why the shift happened, what Webpack does differently, and how to bridge the two.
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.
On a busy store, a careless migration takes the site down by locking a table. A guide to safe schema changes — the locks that bite, adding columns and indexes safely, and the expand-contract pattern for renames and NOT NULL.
When a store needs search, the reflex is Elasticsearch — but Postgres full-text search has grown up. A head-to-head for the e-commerce case: ranking, facets, typo tolerance, and the operational cost that decides it.
A catalog page that's fast for ten users can fall over for ten thousand. A deep-dive on the layers of caching that keep it fast — fragment caching, HTTP/CDN caching, low-level caching — and the hard part: invalidation and stampedes.
Selling across countries means translating not just your interface but your data, your URLs, your formats, and your SEO. A deep-dive on Rails i18n at scale — locale files, translated content, locale routing, and the traps.
A product catalog is mostly images, and naive image handling will sink it. A deep-dive on building an image pipeline that scales — keeping originals, generating variants in the background, offloading to a CDN, and not melting your web servers.
Selling in more than one currency sounds like a display problem and turns out to be a data-modelling one. A deep-dive on representing money correctly, storing prices across currencies, exchange rates, rounding, and the mistakes that cost real money.
Rails 5 has reached its release candidate. A tour of what's new — first-class API-only apps, ActionCable, ApplicationRecord, the attributes API, required belongs_to, and the rails command swallowing rake.
Rails 5 brings ActionCable — WebSockets integrated into the framework. A deep-dive on channels, subscriptions, and broadcasting, how the Redis-backed pub/sub works, and the scaling realities of holding thousands of open connections.
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.
Ruby 2.3.0 lands on Christmas Day with two changes you will type every day — the frozen-string-literal pragma and the &. safe-navigation operator — plus dig, did_you_mean, and more. A practical tour.
Long-lived feature branches cause painful merges and big-bang releases. Feature flags let you merge to trunk continuously and decouple deploy from release. A guide to the workflow, the kinds of flags, and managing flag debt.
The N+1 query is the most common performance bug in Rails apps, and it hides perfectly in development. A deep-dive on what causes it, the difference between preload/eager_load/includes, counter caches, and how to catch it automatically.
Every Rails request passes through a stack of Rack middleware before it ever reaches your controller. A deep-dive on what Rack is, how the middleware stack works, and how to write your own for logging, auth, and more.
SASS gives you power; architecture is what stops you abusing it. A deep-dive on structuring stylesheets so they scale — design tokens, the 7-1 folder pattern, BEM, managing specificity, and thinking in components instead of pages.
SQL LIKE is not search. A deep-dive on adding real full-text search to a Rails app with Elasticsearch — analyzers, mappings, relevance, keeping the index in sync — and an honest take on when Postgres is enough instead.
Capistrano automates deployment over SSH with atomic releases and one-command rollbacks. A practical guide to the releases/current model, shared files, the Rails task chain, multistage config, and the gotchas worth knowing.
Skinny controllers pushed all the logic into fat models, and now they're unmanageable. Service objects are the most popular fix — here is how to use them well, when they help, and how they turn into a worse mess if you overdo it.
Sidekiq processes background jobs with threads and Redis, and it is fast — but the speed comes with rules. A deep-dive on how it works, why your jobs must be small and idempotent, and how to survive retries and concurrency.
Postgres 9.4 brings JSONB — indexed, queryable JSON inside a relational column. A deep-dive on when to reach for it, how to query and index it, the Rails 4.2 integration, and the trap of treating your SQL database like a document store.
A war story about standing up the infrastructure behind a cryptocurrency project — coin daemons, wallets, and key handling — reproducibly, with shell scripts and Ansible. The security lessons that only a money-handling system teaches.
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.
The Rails 4.2 release candidate is out. A look at the three changes that matter most — a unified background-job API, a quietly large performance gain in ActiveRecord, and foreign keys finally landing in migrations.
Why a static site is the right tool for a developer blog, and how to build one with Jekyll and host it free on GitHub Pages — front matter, Liquid layouts, Markdown posts, and writing in the same workflow you already use for code.
AngularJS makes the DOM react to your data automatically. A practical introduction to two-way binding, directives, controllers, and dependency injection — plus an honest look at the digest cycle and where the magic costs you.
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.
Ansible configures servers over plain SSH with no agent to install. A practical guide to provisioning a Ruby app server with idempotent playbooks and reusable roles — and why agentless won us over.
Clicking through the AWS console does not scale past a handful of servers. How we drive an EC2 fleet from Ruby — declaring instances as data, reconciling desired against actual, and tagging your way to sanity.
Rails 4.1 is a quietly excellent point release. A tour of the features we adopted immediately — config/secrets.yml, ActionMailer previews, ActiveRecord enums, the Spring preloader, and request variants — and the small print on each.
Docker is approaching 1.0 and it is about to change how we ship Ruby apps. A hands-on guide to images, containers, and Dockerfiles — building a reproducible dev environment and why 'works on my machine' is finally on its way out.
Cucumber is either a bridge to your domain experts or an expensive layer of indirection — and which one depends entirely on how you write it. Here is where it earns its keep, where it doesn't, and the rules we follow.
Users misspell things, and exact matching fails them silently. A practical deep-dive into trigram-based fuzzy matching in Ruby — how it works, how to make it fast, and when to push it into Postgres instead.
Two tools we reach for on every Rails project. How HAML makes templates readable and SASS keeps stylesheets maintainable — with the patterns that work and the traps (over-nesting, @extend) that don't.
Ruby 2.1.0 shipped on Christmas Day, as tradition demands. The headline is a generational garbage collector, but the language changes — required keyword args, refinements, Exception#cause — are what will change how we write code.
Your test suite runs on one core while the other three sit idle. A thorough guide to splitting RSpec across processes with parallel_tests — separate databases, runtime-balanced groups, the isolation bugs it surfaces, and scaling it out across CI nodes.
A weekend project: a tiny command-line password vault built on Ruby and OpenSSL. A thorough tour of key derivation, IVs, authenticated encryption, constant-time comparison — and every way we found to get crypto silently, confidently wrong.
We have been shipping MongoDB with Mongoid on real projects. Here is how document modelling really differs from ActiveRecord — embedded vs referenced, the missing joins, indexes, atomic writes, consistency boundaries, and where schemaless quietly bites.
Rails 4.0 has just shipped. Here is a thorough, from-the-trenches look at what we are adopting on real projects — strong parameters, the Turbolinks trade-off, Russian-doll caching, first-class Postgres, and the upgrade gotchas.
A talk I gave at the London Ruby User Group introducing Data Context Interaction (DCI) and role injection in Ruby & Rails.
A Ruby design pattern for extending objects with new behaviour at run-time, using modules and singleton classes — illustrated with an RPG character.
Create flexible, scalable 3D-looking buttons with CSS3 gradients and a handy Sass mixin — no images required.