Inspired by: @marckohlbrugge's analysis of Fizzy by 37signals
study the architecture of this rails app. does it differ from a typical rails app? think hard.
- Repository: searlsco/posse_party
- Model: Claude Sonnet 4.5 (
claude-sonnet-4-5-20250929) - Date: 2025-12-21
- Tool: Claude Code CLI
An in-depth analysis of how this Rails application differs from typical Rails architecture.
This application uses "Anemic Models, Skinny Controllers, Service Objects for Everything" instead of the traditional Rails "Fat Models, Skinny Controllers" approach.
The most significant architectural difference is the use of app/lib/ containing 51 service object classes (1,695 total lines). This is unusual because:
- Standard Rails puts service objects in
app/services/orlib/ - This app places them in
app/lib/which is auto-loaded (configured inapplication.rb) - The traditional
lib/directory only contains middleware and rake tasks
Custom Middleware:
lib/middleware/conditional_get_file_handler.rblib/middleware/notifies_admins_on_exception.rb
Additional Non-Standard Directories:
db/queue_migrate/- migrations for Solid Queuesite/- Hugo static site generator for documentation (completely outside Rails)
Models are deliberately thin and focused on relationships/validations only:
Example models:
app/models/post.rb- 16 linesapp/models/account.rb- 52 linesapp/models/crosspost.rb- 34 lines
Models delegate behavior to service objects:
def platform
PublishesCrosspost::MatchesPlatformApi.new.match(self)
endNo model concerns - both app/models/concerns and app/controllers/concerns are empty (just .keep files)
Controllers are thin wrappers that delegate to service objects:
Pattern from app/controllers/accounts_controller.rb:
def create
result = PersistsAccount.new.create(user: current_user, params: params)
if result.success?
flash[:alert] = result.backfill_message unless result.backfill_success?
redirect_to accounts_path, notice: "Account created successfully"
else
@account = result.account
flash[:alert] = result.error_messages.join(", ")
render :new, status: :unprocessable_content
end
endController inheritance:
ApplicationController- base authenticationMembersController- authenticated usersAdminController- admin-only featuresApiController- API endpoints
Naming Convention:
- Verb-based names:
CreatesCrosspostForPost,PublishesCrosspost,FetchesFeed,DeletesUser - Present tense verbs (not "Create" but "Creates")
creates_crosspost_for_post.rbdeletes_user.rbchecks_feed_now.rb
Major service objects have nested namespaces with helper classes:
PublishesCrosspost (main class) with 9 supporting classes:
publishes_crosspost/applies_format_string.rbpublishes_crosspost/composes_crosspost_content.rbpublishes_crosspost/finishes_crosspost.rbpublishes_crosspost/matches_platform_api.rbpublishes_crosspost/munges_config.rbpublishes_crosspost/truncates_content.rb- etc.
FetchesFeed (main class) with 8 supporting classes:
fetches_feed/gets_http_url.rbfetches_feed/parses_feed.rbfetches_feed/creates_crossposts.rbfetches_feed/persists_feed.rb- etc.
Two result object classes for handling success/failure:
result.rb- returns data on successoutcome.rb- returns message on success
crosspost_config.rb- Struct with 29 keyword arguments
app/lib/platforms/base.rb - Template method pattern
Platforms::BskyPlatforms::XPlatforms::MastodonPlatforms::ThreadsPlatforms::InstagramPlatforms::FacebookPlatforms::LinkedinPlatforms::YoutubePlatforms::Test(test environment only)Platforms::TestFailure(test environment only)Platforms::TestSkipped(test environment only)Platforms::Null(for testing)
Each platform has its own namespace with specialized services:
platforms/bsky/syndicates_bsky_post.rbplatforms/bsky/assembles_rich_text_facets.rbplatforms/youtube/uploads_youtube_video.rbplatforms/threads/creates_container.rb- etc.
publishes_crosspost/matches_platform_api.rb - Registry pattern
searls-auth- Authentication engine (mounted at/auth)
solid_queue- Job backend (Rails 8 default)mission_control-jobs- Job monitoring UI
bskyrb,didkit- Blueskyx- X/Twittermastodon-api- Mastodon- Direct HTTP for others (Threads, YouTube, etc.)
feedjira- RSS/Atom parsinghttparty- HTTP clientreverse_markdown,redcarpet- Markdown conversion
- Uses
bcryptdirectly instead of Devise - Uses
propshaftinstead of Sprockets turbo-rails+stimulus-rails- Hotwire stacktailwindcss-rails- CSS framework
config/initializers/invariant_assumptions.rb- Runtime assertions about Rails internalsconfig/initializers/cors.rb- CORS configurationconfig/initializers/feedjira_ext.rb- Extends external gemconfig/initializers/ses_api_delivery.rb- Custom email delivery
config.autoload_lib(ignore: %w[assets tasks])
config.active_record.strict_loading_by_default = true
config.active_record.strict_loading_mode = :n_plus_one_only- Swaps
ActionDispatch::Staticwith customMiddleware::ConditionalGetFileHandler - Inserts
Middleware::NotifiesAdminsOnExceptionbeforeActionDispatch::DebugExceptions
Standard Rails view structure with domain-organized partials:
app/views/accounts/app/views/posts/app/views/crossposts/app/views/shared/- 26 shared partialsapp/views/icons/- 31 icon partials
Uses Turbo Frames/Streams extensively
101 test files using:
- Minitest (default Rails)
mocktail- Mocking library (not Mocha/RSpec mocks)capybara+capybara-playwright-driver- System testsvcr+webmock- HTTP mockingsimplecov- Coverage
- Feed - RSS/Atom source
- Post - Individual entry from feed
- Account - Social media platform account
- Crosspost - Syndication of post to account
- Notification - User notifications
CrosspostConfig- 29-field configuration StructResult/Outcome- Operation result objectsConstantsmodule - Domain constants
Crosspost status: ready → wip → published/failed/skipped
CheckFeedsJob- Periodic feed checkingFetchFeedJob- Fetch individual feedKickOffCrosspostsJob- Start crosspost workflowPublishCrosspostJob- Publish to platformFinishCrosspostJob- Finalize (for async platforms like YouTube)RequeueAbandonedWipCrosspostsJob- Error recovery
This Rails application demonstrates:
- Service Object Architecture - Heavy use of single-purpose service objects instead of fat models
- Strategy Pattern - Platform abstraction with polymorphic behavior
- Result/Outcome Pattern - Explicit success/failure handling
- Composition Over Inheritance - Service objects compose other service objects
- Template Method Pattern - Base platform class with overridable methods
- Registry Pattern - Platform matching system
- Anemic Domain Model - Deliberate choice to keep ActiveRecord models thin
- Command Pattern - Service objects act as commands
- State Machine - Crosspost workflow management
- Hexagonal Architecture Influence - Platform adapters isolate external dependencies
Instead of the traditional "Fat Models, Skinny Controllers" approach, this uses "Anemic Models, Skinny Controllers, Service Objects for Everything" - a more Domain-Driven Design approach adapted for Rails.
This architecture provides:
- Clear separation of concerns
- Highly testable, single-purpose objects
- Easy to understand business logic flow
- Platform abstraction for multiple social media APIs
- Flexibility to add new platforms without modifying existing code
Reddit post with comments from Justin: https://www.reddit.com/r/rails/comments/1psbh26/comment/nvjalpp/?context=1