The difference between Rails Plugins, Extensions, Gems, Railties, and Engines

There’s overlapping terminology that describes the act of packaging up some new behavior for Rails. I think of two gems I maintain that are of vastly different scales

  • activerecord-has_some_of_many which adds two new tiny association methods to Active Record models in 150 lines of code.
  • GoodJob, which is an entire Active Job backend with a mountable Web Dashboard and database models and custom job extensions in 10k lines of code.

I was pondering the different terminology because I recently saw both ends of the spectrum discussed in the community:

  • A developer on Reddit announced a tiny new gem and a commenter wrote well actually, in your Readme you called it an Engine but you shouldn’t do that.
  • I got pinged on a Rails issue that left me with the belief that some behavior, if not packaged as an Engine, could be expected to break.

I think there are only two dimensions to consider when picking the correct terminology:

  • How the behavior is packaged
  • Whether it’s necessary to package the behavior that way. Which isn’t even a criticism in my opinion, just an observation.

Here’s my opinionated list, in order of somewhat increasing complexity:

  • Rails Extension: A small monkeypatch or tiny new behavior to existing Rails behaviors (Active Record, Active Job, etc.). Especially if it’s not even a gem: simply a file you wrote a blog post about that gets copied into config/extensions and then require_relative’d in config/application.rb.
  • Rails Gem : Reductively, a gem is a load path for some code, and some ownership metadata, and maybe it’s been published to Rubygems.org. Nothing special.
  • ⭐️ Rails Plugin. A generic name covers all situations imo, regardless of size, scope, or complexity.
  • Railtie: When you write a gem that plugs into the Rails framework, you create special file named lib/railtie.rb that has a class that inherits from `Rails::Railtie that contains a DSL to configure how your gem’s behavior interfaces with Rails (configuration, initialization, etc.). I think Railtie is a bit of an odd-duck terminology-wise, but it makes sense considering…
  • Rails Engine: An “Engine” is nearly identical to a Railtie, but the file is named lib/engine.rb and it has a class that inherits from Rails::Engine. But Rails::Engine itself inherits from Rails::Railtie, so this is a matter of degrees. Your gem absolutely needs to use the Engine behavior if it wants to create mountable routes (though I guess you can mount a vanilla Rack app) or inherit from Rails Base classes like ActiveRecord::Base, ActionController::Base, ActiveJob::Base, etc. which live in the Engine’s own app/ directory.

(I’ll clock that the The Rails Guides, under the “Extending Rails” section, has separate guides for Plugins and Engines; the former somewhat surprisngly does not mention the latter.)

So if I go back to the two reasons why I wrote this, and try to be strict with this terminology:

  • If your Plugin has an engine.rb file, it is an Engine. Simple as that. If you don’t need the Engine-specific behavior, you could package it as a Railtie, but I think the difference is negligible.
  • If you don’t have any dependencies on Rails (outside of maybe ActiveSupport) and don’t need to hook into the parent application’s configuration or initialization or framework, then you don’t need a Railtie or Engine at all. Just say it’s a gem that’s compatible with Rails and explain how to use it in that environment.
  • Really, do what you want and tell people about it.

Recently, June 8, 2025

Big business news for my startup: “Frontdoor Benefits Receives $2.1 Million Investment to Improve Access to SNAP and Public Assistance Programs”. My cofounder Charlotte also added some details on our blog too. Big milestone, and onto the next milestone and so forth.


We went to a Golden State Valkyries basketball game last week. It was a lot of fun, and a nice seat was the cost of a substantially-less-nice seat to see the Golden State Warriors. We bought sweatshirts, so that means we’re going back.

On sports, since my last Recently we also went to two Giants games and a Warriors game (whomp whomp).


Of random technical trivia, I discovered a cause of flaky tests: Turbo-Rails debounces broadcast refreshes and it’s possible for them to slip out of test transactions and cause chaos. I have reported it.

Also, my prior-colleague Issy wrote a nice thing and mentioned me.


I finished reading Martha Well’s reissued The Emilie Adventures, which for all her books held my attention the least. I also read Brittany Newell’s Soft Core which was very good. I’m now reading The Future of Another Timeline.

For nonfiction, I (re-) read the updated edition of The Strategy is Delivery. And now I’m greatly enjoying Rouse’s Game Design: Theory and Practice (2nd Edition) which I found because it was referenced in the Digital Antiquarian’s history of the Sierra studio.


I beat the game Yoku’s Island Express, which was a delightful experience on my desired theme of nice metroidvania (recommendations please). I also just started Season 2 of Playdate, which was a reason to find and charge it. On the theme of small handhelds, I ordered an Adafruit USB C Resistor Fixer (say that 5 times fast) for the RGB30 that was collecting dust until I discovered some Pico 8 games I’d like to play.

A nice email for subscribers

I got this nice email from Defector, an online publication I pay for (along with De Programmatica Ipsum, Garbage Day, and Today in Tabs. I think that’s it, though I guess I can include Rubyland.news and Short Ruby Newsletter too). I yearn to share something like this for everyone who actively, if not always monetarily, supports my own work. The feeling is there, though not words this nice.

Subject: Thank you for supporting Defector

I like when people ask me how Defector is doing. Thanks to subscribers like you, I get to say that not only that Defector is doing well, but that I love my work.

I spent most of my life as a writer in jobs that were always and obviously impermanent. Roughly every 20 months, the people running things—people I never saw, and who had no interest in the work we did—would get bored, or nervous, or fall for some dippy executive fad, and pivot some number of us out of our jobs. Basically every innovation in media over the last few decades has been some version of this: an attempt by executive types to see how much less they can do, and how long they can get away with making everything smallersadder, and worse.

The most fundamental benefit of owning your workplace, as we do at Defector, is that we just don’t have to worry about that. Instead, we’re trying to solve the opposite problem: how to make the site better and broader and more surprising and more fun, tinkering towards the right balance of smart and stupid. If you replace mystified Executive Maneuvers with the less abstract challenge of “writing good blogs for people who like to read,” this turns out to be both a good job and a viable business.

But even after filtering out the noise from upstairs, there are plenty of other things to worry about. Chief among them is making sure that those good blogs reliably get in front of both existing subscribers like you and new ones we’d like to bring into the fold. All of those bad, previous jobs happened on an internet that worked better than our current one. Then as now, the people in charge were craven and nasty and dumb, but it was easier to find things, and there were notably more non-toxic places in which to talk about it all.

So: thanks again for reading our blogs, and thanks especially for sharing them with people you think should know about Defector. (Here I’ll remind you that subscribers get an unlimited number of gift links each month to facilitate sharing.) Thanks also for making this the best job I’ve ever had. I’m very lucky to be able to keep doing this, with my friends and for such an engaged community of readers. None of it would exist without you.

Be well, David

Recently, May 4, 2025

Frontdoor Benefits, my new startup, posted our first newsletter/update. In doing our “planning” for it I took a lot from how I apprach this blog:

  • We are writing to a single individual (”Dear treasured friend” not “Hey friends”).
  • We are writing from each of our personal voices (”This is Charlotte. I have… ” or “This is Ben. I have…”)
  • Tone is casual and informal, we are sharing the things we’ve done or are thinking about with a close and curious person.
  • We write about what we have done or thought about. Not so much what we intend to do in the future (definitely no commitments!)
  • We try to avoid talking about the newsletter itself. For example, we don’t say how frequently we want to publish, or apologizing for it being a minute. Every post of the newsletter just is.

It’s been successful so far, in that we asked for some things in it and people have delivered.


I have been doing lots of research and having interesting conversations about customer/client/caseworker/constituent relationship management. Not so much “salesforce” or “hubspot” but how to break down identities and artifacts and tasks; differentiating between task-based from case-based models, and worker activity from supervisory concerns. If you have experience, or opinions, or related traumas, I’d love to chat: [email protected].


I discovered that turbo-rails debounces Refresh Broadcasts; after being surprised for many, many minutes why a Rails runner script wasn’t broadcasting as expected. One of those things that simply manifests as strangely broken if you don’t go and read the code.


My new workday is settling down into routine, and now I’m trying to add a regular morning swim 3 days a week. I managed one. Progress!


I finished reading Space Between Worlds and its sequel Those Beyond the Wall. And started Kirstin Chen’s Counterfeit , which so far has me wondering whether phrases such as “nipple-length hair” is attributable to the character or the author (it was no mystery at all the last time this came up).

Katamari Damacy is on Apple Arcade, and I’ve been playing that. I also picked up The Long Dark again which I haven’t played since early access before they added a story mode; as a walking simulator it’s a little slow and bleak and I’m not a big fan of graphically dying… but it is a walking simulator and I like that.

Last night we saw Empire of the Sun at the Greek theatre.

I renewed my CPR training and certification. That’s content.


We’re fostering Dennis, a sweet tabby cat who needs a forever home.


Fuck, AmeriCorps is gone.

Recently, April 20, 2025

Angelina caught a cold, so the past week has been largely laying low and sleeping 9+ hours a night trying not to catch it myself. Not the worst life.


Elevating this to top fish recipe: Rockfish, Garlic, Shallots, Tomatoes & and a lotta Herbs.


Using ChatGPT’s Web Search is ok. “Find me articles, marketing posts, and conference talks about [something]”. I have to follow up several times slightly differently (“anything else? What about lightning talks?”) and copy resulting links into a separate doc to organize to have something approaching comprehensive…. But pretty good and better than what I can get out of either Kagi or Google. I ignore the summaries and chatty nonsense and just copy the links and read them myself. Sorry climate and future generations.


I cut ~30 seconds from my GitHub Actions build times by replacing my apt-get install step with an action that caches using awalsh128/cache-apg-pkgs-action; there’s a couple options but this one had the most stars in the marketplace:

# Before
- name: "Install packages"  
  run: |
  sudo apt-get -yqq update
  sudo apt-get -yqq install libvips-dev

# After
- name: "Install packages" 
  uses: awalsh128/cache-apt-pkgs-action@7ca5f46d061ad9aa95863cd9b214dd48edef361d
  with:
  packages: libvips-dev
  version: 1 # cache version, change to manually invalidate cache

Turbo/hotwire stuff: I’ve been gradually replacing more granular broadcasts of like prepend/update/remove with page refresh events for their simplicity. The challenge I have is that if there is a form + refreshable content on the same page (sometimes with the form in the middle or multiple forms). If the content refreshes, I don’t want to refresh the form. But I do want the form to refresh itself when submitted (show validation messages, reset, etc.). I can wrap the form in a data-turbo-permanent for the first part, but then the form doesn’t update when it’s submitted.

My workaround to that is a stimulus controller that wraps the form and removes the data-turbo-permanent when the form is submitted, inspired by this. Is there a better way to do it?

import { Controller } from "@hotwired/stimulus"

// To be used to wrap a form to allow the form to be permanent during
// Turbo Stream refresh events but to update normally when submitting the form.
// Example:
//  <div data-turbo-permanent id="<%= dom_id(@phone, :message_form) %>" data-controller="permanent-form">
//    <%= form_with ...
//  </div>

export default class extends Controller {
  connect() {
    this.submitHandler = this.submitForm.bind(this);
    this.element.addEventListener("submit", this.submitHandler);
  }

  disconnect() {
    this.element.removeEventListener("submit", this.submitHandler);
  }

  submitForm(event) {
    if (event.target.matches("form")) {
      this.element.removeAttribute("data-turbo-permanent");
    }
  }
}

I finished Spinning Silver. Now reading The Space Between Worlds.

I bought Javascript for Rails Developers, largely because I like the posts on Rails Designer.

I started the demo for Unbeatable (“where music is illegal and you do crimes”); I like the art style, but is it fun? I dunno.


I had to go to the shipping warehouse to pick up my new mechanical keyboard because I kept missing the delivery person, but it otherwise arrived no problem.

Recently, April 14, 2025

Last week I tried out a lot of coworking spaces: Canopy, Tandem, Temescal Works. We’re trying to find a space between Oakland and SF with nice outdoor walks.


I’m having a great time being a technical cofounder to my (everything else!) cofounder. It’s fun explaining what I am doing. And we have fun shouting “Monolith!” and “Skateboard [MVP]” all day long.

An example of an explanation I gave: one of our client advocate tools is a Twilio-powered Voice Conference Bridge where we can dial in any number of participants which helps shadow and assist our clients in their welfare application journey. We wanted to add DTMF tones for dialing extensions and navigating IVR systems. Unfortunately, the Twilio API that I used initially (Create a Conference Bridge, then create a Participant Call) doesn’t support DTMF tones so I had to flip the logic to a different API (Create a Call, then add it to a Conference Bridge as a Participant). Figuring that out was a couple hours of reading docs and SDK code, feeling confident I wasn’t overlooking something, creating a runner script to bench test it, and finally putting the pieces into their production-ready places which was only like 20 lines of code at the end. That’s where the time goes.


I had several conversations about “the AI memo”. I’ll paste the two themes I talked about, in the words I put into the Rails Perf Slack:

I don’t know what Shopify’s culture is, but I imagine the pronouncement itself could be useful, for Tobi.

As a leader, you say “everyone must… unless you get an exception from me” to learn by forcing exceptions to roll up to you directly. It’s a shitty way to learn, but power is shitty. (I mean “learn” in the very personal sense). It’s a tactic. The flip side is then as a leader you debug the need for the exceptions and that leads to a better policy.

GitHub’s CEO said (not published) something similar (internally) 2 weeks before I left. I sweated it for a day, then DMed him and said “as a manager, I’m not aware of any LLM api that is approved for my use for internal admin stuff?” and he pointed me to the GitHub Models product that is totally unreferenced on any of the internal docs about staff AI tools. I poked that enablement team to add it, and I dunno if the CEO actually followed up with anyone to debug the low awareness (the story of my DM got retold at a different meeting as one about security, but it was really my complete unawareness and its absence on any of the tool lists that were intended to be the starting place for staff to integrate AI into their work).

TLDR: in a culture of opennness (safety to DM the CEO about the policy) and learning (the policy is the start not the end of discussion). I could see the pronouncement to be catalytic.

And

I appreciate that FOMO hype (“don’t be left behind”) has been largely absent [in this Slack community], though I find it elsewhere and a huge distraction.

I think a lot in this thread could have the word “AI” replaced with “Rubymine” and it would be an equally familiar discussion between folks who use it, folks who are curious, and folks who are happy with their current code editor and wish others would stop pushing Rubymine cause it’s slow and costs money and makes developers lazy, analogously.

I share that because I don’t think it’s a new experience to be like: “both of us are producing software but our moment-to-moment experience is wildly materially different” (eg “here is my elaborate process for naming and organizing methods so I can find them later” vs “I cmd-click on it and I go there”). … and then people debate whether that difference matters or not in the end.

When I think of my own experience in The Editor Wars I think the only meaningful thing is to go pair with somebody and observe their material experience producing software in situ, fumbles and all.

I did my first Deep Research this week; it was good A1.


Week one of my startup journey and I already made a successful Rails PR with a bug fix. I didn’t think it was a big deal but it got backported too 💪


On Saturday I did what I’m trying to make my standard 10-mile hike: Stinson Beach to Muir Woods loop (Steep Ravine up, Bootjack down, Ben Johnson up, Dipsea back down). Shandy and fries at the end.

Sunday was a swim (the Bay was a balmy 57F/14C !) and the treat of a Warriors day game with Angelina’s geospatial colleagues, and dinner and ice cream and showing them all our favorite park walks.


I’m still reading Spinning Silver; it’s good and long! I have not played Witcher 3 since writing about it last time, or really anything.

Recently, April 7, 2025

  • I had my last day at old job. I got locked out of all my GitHub accounts at noon on Friday. At 2pm I did a tour of a coworking space for my new job. We’re looking at several spaces between where I live (SF) and my cofounder (Oakland). Both of us are looking forward to regularly being in the same space with a big whiteboard adjacent to somehere nice to walk around outside.
  • I helped publish the monthly April Newsletter for the Alliance of Civic Technologists. I’ve stepped back mostly to focus on website tasks, though I’m proud that the comms stuff I previously pushed on (“what if we just regularly re-published stuff from the network without committing to a lot of other words?”) seems to have been taken up. I also feel like my involvement has been good training for my conviction of like “the reason we’re doing it this way is because I’m responsible for it.” Not that I expected to defend a a five page website with Jekyll on GitHub pages in 2023 (when I put together with Bill Hunt and Molly McLeod), but the only way some people know how to engage is by aggressively wondering why you didn’t do it differently.
  • I tried not to think about (new) work all weekend. Saturday we got up before 5am to volunteer at a Bay Bridge swim; we worked registration and body marking (TIL some people are immune to sharpie). We took a dip ourselves, cafe for breakfast, then farmer’s market, cleaned up at home, met friends for tea (one of whom I’m trying to recruit to work with me; so it goes now), then to the protest, then a wine bar where we picked up some more friends in civic tech, then a gallery showing for some other friends from the swim club, then scrambled eggs at home for dinner. Saturday! Sunday was more sedate of swim, cafe, walk to Trader Joes, a different wine bar where I found agreement with a neighbor that being run over by a car is one’s most likely fate in SF.
  • I got up a LinkedIn post about my job change:

    Today was my last day GitHub. I’m really proud of the last 3 years helping build and support the Rails and Ruby developer community inside of GitHub and beyond.

    I also couldn’t pass on the new opportunity to work again on improving America’s social safety net. It’s been 3 years since I left Code for America and I’m excited about new options that have opened up with tech, telephony, and AI. I’m optimistic that we can fully close the loop in assisting, advocating, and escalating for people throughout their welfare journey and achieve significantly higher approval rates than was possible before. And do so sustainably; that’s the challenge!

    Here’s a nice write-up about what my cofounder and I are hoping to achieve.

  • I participated in totally normal global commerce by ordering a mechanical keyboard (75% Alice brown). It’s currently in Guangzhuo; we shall see what happens now.
  • I finished reading Polostan. It’s better than his last… 4 books, despite containing the phrase “girls’ bottoms in riding breaches” two times too many. I started Naomi Novik’s Spinning Silver.
  • I started playing “The Witcher 3” which is neither cozy nor casual. I don’t know how many of the Witcher books I read previously because all evidence points to it being prior to 2014 when Pantheon’s new-hire perk was a Kindle. Seems like there are more books now.
  • We watched the White Lotus finale 🤷
  • On my first day as CTO, I reviewed all of our seat-based SaaS costs. $8 here, $4 there, $15 jeez 🫠 I’m already annoyed that my former employer charges for Branch Protection rules to block force-pushes on main 🙃

Recently, April 2, 2025

  • I’ve been away from work for the past week hosting family, including a 9 and 11 year old. In that week, we did: Ferry Building Farmer’s Market, Exploratorium and Tactile Dome, Alcatraz, “Dear San Francisco” at Club Fugazi, swam in the Bay and at the YMCA, rode a cable car, rode some buses, walked the Golden Gate Bridge, hiked Muir Woods, ate House of Prime Rib, Mama’s, Fish, Tailor’s Son, and Cafe de Casa. We had the kids for a night so their parent’s could do Napa and overnight at Indian Springs. I dropped them off at the airport yesterday and it is blessedly quiet and cats are decompressing.
  • For the kids we opened up The Big Bag of Quest Headsets that we have accumulated because Angelina works on them. Lots of charging and battery swapping and then Beat Saber. The kids also played Threes and Tiny Wings on iPhones.
  • During downtime we watched through “Wolf King”, and I got to provide adult commentary of “do you think they are a werelord?” about everyone; I had fun.
  • I finished reading Wicked; I won’t be doing the trilogy. I recluctantly started reading Polostan; the past several Neal Stephenson books have not been my thing but I am a suffering optimist.
  • I started playing Anodyne. Please suggest casual uncomplicated metroidvanias and open-world wander-arounders.

Wide Models and Active Record custom validation contexts

This post is a brief description of a pattern I use a lot using when building features in Ruby on Rails apps and that I think needed a name:

Wide Models have many attributes (columns in the database) that are updated in multiple places in the application, but not always all at once i.e. different forms will update different subsets of attributes on the same model.

How is that not a “fat model”?

As you add more intrinsic complexity (read: features!) to your application, the goal is to spread it across a coordinated set of small, encapsulated objects (and, at a higher level, modules) just as you might spread cake batter across the bottom of a pan. Fat models are like the big clumps you get when you first pour the batter in. Refactor to break them down and spread out the logic evenly. Repeat this process and you’ll end up with a set of simple objects with well defined interfaces working together in a veritable symphony.

I dunno. I’ve seen teams take Wide Models pretty far (80+ attributes in a model) while still maintaining cohesion and developer productivity. And I’ve seen the opposite where there is a profusion of tiny service objects and any functional change must be threaded not just through a model, view and controller but also a form object and a decorator and several command objects, or where there is large number of narrow models that all have to be joined or included nearly all of the time in the app—and it sucks to work with. I mean, find the right size for you and your team, but the main thrust here is that bigger doesn’t inherently mean worse.

This all came to mind while reading Paweł Świątkowski’s “On validations and the nature of commands”:

Recently I took part in a discussion about where to put validations. What jarred me was how some people inadvertently try to get all the validation in a one fell swoop, even though the things they validate are clearly not one family of problems.

The post goes on to suggest differentiating between:

  • “input validation”, which I take to mean user-facing validation that is only necessary when the user is editing some fields concretely on a form in the app. Example: that an account’s email address is appropriately constructed.
  • “domain checks”, which I take to mean as more fundamental invariants/constraints of the system. Example: that an account is uniquely identified by its email address.

I didn’t entirely agree with this advice though:

In Rails world you could use dry-validation for input validations and ActiveRecord validation for domain checks. Another approach would be to heavily use form objects (input validation) and limit model validations to actual business invariants.

My disagreement is because Active Record validations have a built-in feature to selectively apply validations: Validation Contexts (the on: keyword) and specifically custom validation contexts:

You can define your own custom validation contexts for callbacks, which is useful when you want to perform validations based on specific scenarios or group certain callbacks together and run them in a specific context. A common scenario for custom contexts is when you have a multi-step form and want to perform validations per step.

I use custom validation contexts a lot. I don’t intend for this to be a tutorial on custom validation contexts, but just to give a quick example:

  • Imagine you have an Account model
  • A person can register for an account with just an email address so they can sign in with a magic link.
  • An account holder can later add a password to their account if they want to optionally sign in with a password
  • An account holder can later add a username to their account which will be displayed next to their posts and comments.

You might set up the Account model validations like this:

class Account < ApplicationRecord
  validates :email, uniqueness: true, presence: true
  # also set up uniqueness/not-null constraints in the database too
  validates :email, email_structure: true, on: [:signup_form, :update_email_form]

  validates :password, password_complexity: true, allow_blank: true
  validates :password, presence: true, password_complexity: true, on: [:add_password_form, :edit_password_form]

  validates :username, uniqueness: true, allow_blank: true
  validates :username, presence: true, on: [:add_username_form, :edit_username_form]
end

Note: it’s possible to add custom validation contexts on before_validation and after_validation callbacks, but not others like before_save, after_commit, etc. only take the non-custom callbacks like on: :create.

So to wrap it up: sure, maybe it can all go in the Active Record model.

Recently, March 26, 2025

  • I am on a new work adventure. I gave my notice at GitHub and will be doing this full-time starting in April. The new job should be a nice combination of a cozy “this again” and some thrilling new.
  • I finished reading Careless People; recommend as a good sequence of business trainwrecks that will leave you wondering if this one is penultimate trainwreck (spoiler: it’s not). Now I’m reading Wicked; I didn’t really like the beginning but it’s gotten more interesting.
  • I finished Severance. Hopefully without spoilers, the consistent plot driver seems to be “Mark (yes) sucks”. So now just White Lotus and with palate cleansers of Say Yes to the Dress.
  • I have been desultorily playing Bracket City; the scoring system generates no motivation for me but it’s fun to have found another use for the decades spent training my brain to parse deeply nested hierarchical syntax. I was also told that LinkedIn has games, and other than being “Faster than 95% of CEOs” at Queens, I have already lost my streak.
  • I asked on Rails Performance Slack how to better delegate Rails model association accessors and got some good ideas.
  • My RailsConf session proposal was accepted! See you there 🙌

Older posts