Part 3 of 10 5 min read

Use the language your team already knows

Language Strengths Over Language Familiarity

We used to pick the language our team already knew. Agents are fluent in all of them, so now I pick the language that is actually best at the job.

Years ago I wrote an essay about why we taught Ruby first. One line from it I still believe completely: "Don't define yourself by your first programming language."

I went further. I wrote that "programming languages are just our tools," and I quoted Dijkstra: "The tools we use have a profound (and devious!) influence on our thinking habits." Pick the tool for the problem. Don't let the tool pick the problem for you.

I meant every word.

And then I reached for Ruby. Every single time.

The web app, sure. But also the scripts, the internal tools, the CLIs, the background workers that probably should have been something else. Not because Ruby was best at those jobs. Because I knew it, my team knew it, and I could hire for it. One language everywhere meant anyone could jump into anything and a new engineer was productive in weeks.

"The right tool for the job" was something I said right before picking the tool I already knew.

The familiarity tax is gone

Familiarity was a real cost, so it was rational to optimize for it. Learning a new language well takes months. Hiring for three languages is harder than hiring for one. Every one of those constraints is a proxy for "humans are expensive to retrain."

The humans aren't writing most of the code anymore.

Claude doesn't have a favorite language. It doesn't need six months to get comfortable with Go. It writes idiomatic Rust as easily as idiomatic Ruby, and it doesn't complain about the borrow checker the way I would.

When the thing writing your code is fluent in everything, familiarity stops being a reason to choose anything. What's left is the question I claimed to be asking all along: what is this language actually good at?

Same CLI, two languages

I wanted a small CLI to manage content on this site from my terminal. List drafts, push a markdown file up as a post, check what's published. It talks to my Rails app's API. Nothing fancy.

Old me writes this in Ruby with Thor without thinking:

ruby
class AviCLI < Thor
  desc "drafts", "List draft posts"
  def drafts
    res = HTTP.auth("Bearer #{ENV["AVI_TOKEN"]}")
              .get("#{BASE_URL}/api/posts", params: { status: "draft" })

    res.parse.each { |post| puts "#{post["id"]}  #{post["title"]}" }
  end
end

That's lovely. It reads like a sentence. It's the kind of code I used to show students to make them fall in love with Ruby.

Here's the same command in Go, with Cobra, which is what I actually shipped:

go
var draftsCmd = &cobra.Command{
	Use:   "drafts",
	Short: "List draft posts",
	RunE: func(cmd *cobra.Command, args []string) error {
		req, _ := http.NewRequest("GET", baseURL+"/api/posts?status=draft", nil)
		req.Header.Set("Authorization", "Bearer "+os.Getenv("AVI_TOKEN"))

		res, err := http.DefaultClient.Do(req)
		if err != nil {
			return err
		}
		defer res.Body.Close()

		var posts []Post
		if err := json.NewDecoder(res.Body).Decode(&posts); err != nil {
			return err
		}
		for _, p := range posts {
			fmt.Printf("%d  %s\n", p.ID, p.Title)
		}
		return nil
	},
}

Longer. Noisier. All that if err != nil. Five years ago I would have held these up side by side and declared Ruby the winner.

I don't write Go. I didn't write that. I read it once, it made sense, I moved on. What I care about now is everything that happens after the code exists:

Ruby + ThorGo
InstallRight Ruby version (rbenv, asdf, or mise), gem install or bundle install, hope nothing needs native extensionsCopy one file
DistributionA gem, plus a Ruby runtime on every machineA single static binary
StartupRoughly a few hundred milliseconds once Bundler loadsRoughly instant, a few milliseconds
Cross-compilingNot really a thing. Each machine brings its own RubyGOOS=linux GOARCH=arm64 go build
DependenciesThor, an HTTP gem, their transitive gems, and whatever OpenSSL the machine hasCobra. The HTTP client and JSON are in the standard library
ExpressivenessBeautiful. Genuinely betterVerbose and repetitive
PrototypingFaster. A script in five minutes, irb to poke at itSlower if a human is typing

I've lost more hours to rbenv and Bundler on random machines than I'd like to admit, and I watched a lot of students lose whole afternoons to it. The Ruby version works great on my machine. Then it's the wrong Ruby on my other laptop. Then there's no Ruby at all on the server.

The Go version: go build, scp it anywhere, run it. That's the whole distribution story.

Look at where Ruby wins in that table. Expressiveness and prototyping speed. Both are about what it feels like for a human to write the code. Ruby was designed, explicitly, to value the programmer over the machine. That was the whole pitch, and it was a beautiful one. But when the agent is doing the typing, the thing Ruby optimized for is happening to someone else. The things Go optimized for, shipping and running, still land on me.

Where Ruby still wins

The Rails app is still Rails. For a web application, nothing I know beats it: the conventions, the generators, Active Record, Hotwire, the whole shape of it. Agents are great at Rails precisely because it's so opinionated. I'm not rewriting this site in Go.

And Ruby is still where I'd go for a throwaway script inside this repo, where Ruby is already installed and the distribution problem doesn't exist. bin/rails runner beats a new Go module for a one-off data fix every time.

I just stopped dragging Ruby into places it doesn't belong because it was comfortable.

Pick by strength

Once familiarity is off the table, most choices get almost boring.

A CLI you want to hand to anyone? Go. A hot path that needs to be really fast, or something that has to be memory safe and small? Rust, as its own little thing, instead of squeezing a few more milliseconds out of Ruby with caching tricks. Anything touching ML or data? Python, because that's where the libraries live and fighting that is pointless. A Mac or iPhone app? Swift, and I have a lot more to say about that in the next post.

None of that is a polyglot flex. It's what I told students to do, finally done.

Where this breaks down: if a team of humans will be on call for this code at 3am, familiarity still matters. Someone has to reason about it when the agent is confused. And stick to mainstream languages with good tooling and lots of public code. Agents learned from what's out there, so they're fluent in Go and Python, not in whatever obscure language you've been wanting an excuse to try.

You also still have to understand what you're shipping at the level of behavior. I don't read every line of that Go CLI, but I know what it talks to and what it's allowed to touch. (More on that in Stop Reading Code.)

I used to need a reason to leave Ruby. Now I need a reason to stay. Dijkstra was right that our tools shape how we think. For years mine shaped which problems I'd even try to solve in anything else.

This is part of a series on everything I'm changing my mind about. Start at Rethink Everything. Next up: Go Native Everywhere.

The whole series

  1. 1Code is written for humans to readStop Writing and Reading Code
  2. 2Never rewrite from scratchRewrites Might Be Better Than Massive Refactors
  3. 3Use the language your team already knowsLanguage Strengths Over Language Familiarity
  4. 4Build it once for the web and wrap itGo Native Everywhere
  5. 5Keep the monolith majesticI Hated Microservices. Agents Love Them.
  6. 6Pay someone else to run your serversOwn Your Infrastructure
  7. 7Don't repeat yourselfDuplicate Code So Your Agents Stop Colliding
  8. 8Types are ceremonyTypeScript, I Don't Hate You Anymore
  9. 9Respect the testing pyramidMore E2E Tests, Fewer Unit Tests
  10. 10Refactor for readabilityStop Refactoring for Humans. Refactor for the Agent.