The engine convention already here

Free preview 4 min to read

Four files in this application declare a Rails engine convention. Twenty-three models decline it. That's the whole starting point.

The declaration

# app/models/catalog.rb
module Catalog
  def self.table_name_prefix
    "catalog_"
  end
end

Four namespaces do this, Catalog, Community, Enrollment and Partners, and each names a prefix. Identity declares the module and no prefix.

table_name_prefix is the Rails engine idiom. It's what isolate_namespace sets up for you, and its purpose is to keep an engine's tables from colliding with a host application's. Adopting it in a monolith means adopting an engine convention without an engine. That's a reasonable thing to do: a catalog_courses table says which context owns it.

What happens in practice

USES the prefix (1):
  Community::Comment -> community_comments

OVERRIDES it (23):
  Catalog::Category           -> categories
  Catalog::Challenge          -> challenges
  Catalog::Course             -> courses
  Catalog::Lesson             -> lessons
  Catalog::Quiz               -> quizzes
  Catalog::QuizOption         -> quiz_options
  Catalog::QuizQuestion       -> quiz_questions
  Enrollment::Badge           -> badges
  Enrollment::Bookmark        -> bookmarks
  Enrollment::Enrollment      -> enrollments
  Enrollment::ExperiencePoint -> experience_points
  Enrollment::LessonCompletion -> lesson_completions
  Enrollment::LessonNote      -> lesson_notes
  Enrollment::Level           -> levels
  Enrollment::QuizAttempt     -> quiz_attempts
  Enrollment::UserBadge       -> user_badges
  Partners::ApiToken          -> partner_api_tokens
  Partners::CourseSponsorship -> partner_course_sponsorships
  Partners::HandoffNonce      -> partner_handoff_nonces
  Partners::Inquiry           -> partner_inquiries
  Partners::Partner           -> partners
  Partners::Referral          -> partner_referrals
  Partners::Role              -> partner_roles

Each of those twenty-three declares self.table_name explicitly, and an explicit table name wins over a prefix.

One model out of twenty-four honors the convention, and it does so by not mentioning it. Community::Comment has no self.table_name line, so the prefix applies and the table is community_comments.

flowchart TB
  A["4 namespaces declare table_name_prefix"] --> B["the Rails engine idiom, in a monolith"]
  B --> C["23 models declare self.table_name and override it"]
  B --> D["1 model omits it and is prefixed"]
  D --> E["Community::Comment -> community_comments"]
  C --> F["the convention was adopted, then declined, one model at a time"]

What this is evidence of

It isn't a bug. Every one of those twenty-three names is correct: courses, lessons and enrollments are good table names, and catalog_courses would be worse to type in a psql session.

It's evidence of a convention adopted without a decision behind it. The framing of the mid-level course "Leader: ADR process" applies directly: the prefix declarations say what to do, nothing says why, and so each model author made the call again, twenty-four times, and reached the same answer twenty-three times.

The one exception doesn't read as a disagreement either. Community::Comment was written on 30 August, the same day as the first four Partners models, and all of those declare their table names. Nothing in the code says why this one doesn't.

That's the real finding: the current table naming isn't a convention. It's what's left of twenty-three independent decisions plus one omission.

The three honest options

Delete the prefix declarations. They take effect in one model out of twenty-four, and otherwise only mislead a reader into thinking tables are prefixed. Rename community_comments to comments and the codebase becomes consistent.

Honor them. Delete the twenty-three self.table_name lines, write twenty-three renaming migrations, and every table announces its context. It's expensive, and it makes the schema self-describing.

Leave it and write down which one is true. One line in MEMORY.md, one ADR: "tables are named for what they hold, not for their context; the table_name_prefix declarations are vestigial."

Any of the three beats leaving it unstated, because otherwise a reader loses ten minutes every time they wonder whether a table is prefixed, and the answer is "usually not, except once."

This application took the third option on 2026-09-28. MEMORY.md now says tables are named for what they hold, that a new model declares self.table_name, and that the four declarations are vestigial.

Why this opens a course about engines

The question underneath is the one this whole course asks: how much of the engine model do you want, and what does each piece cost?

This application took one piece, the table prefix, and then rejected it in practice. That's a specific answer to a specific question, reached without anyone stating either. Lesson 2 covers the rest of the model.

Key takeaways

  • Four namespaces declare table_name_prefix, the Rails engine idiom for avoiding table collisions.
  • Measured on 2026-09-28: 23 models override it with an explicit self.table_name; only 1 honors it, by omission.
  • None of the twenty-three names is wrong; this is a convention adopted without a decision behind it.
  • Three honest options: delete the declarations, honor them, or write down which is true.
  • Today a reader loses ten minutes whenever they wonder whether tables are prefixed.
Lesson 1 of 7 · free

Keep going with lesson 2

What an engine actually is

An engine is a Rails application inside another; isolate_namespace prevents naming collisions but leaves constant references open.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...