Skip to content

Instantly share code, notes, and snippets.

@andyw8
Last active December 23, 2025 12:52
Show Gist options
  • Select an option

  • Save andyw8/da70bb0c8cb6c6a16f6b1085e88a5480 to your computer and use it in GitHub Desktop.

Select an option

Save andyw8/da70bb0c8cb6c6a16f6b1085e88a5480 to your computer and use it in GitHub Desktop.
POSSE Party Rails Application - Architectural Analysis

POSSE Party Rails Application - Architectural Analysis

Inspired by: @marckohlbrugge's analysis of Fizzy by 37signals

Original Prompt

study the architecture of this rails app. does it differ from a typical rails app? think hard.

Analysis Metadata

  • 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.

Key Finding: Service Object Architecture vs Traditional Rails

This application uses "Anemic Models, Skinny Controllers, Service Objects for Everything" instead of the traditional Rails "Fat Models, Skinny Controllers" approach.


1. Directory Structure - NON-STANDARD Organization

Major Deviation: app/lib/ instead of app/services/

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/ or lib/
  • This app places them in app/lib/ which is auto-loaded (configured in application.rb)
  • The traditional lib/ directory only contains middleware and rake tasks

Custom Middleware:

  • lib/middleware/conditional_get_file_handler.rb
  • lib/middleware/notifies_admins_on_exception.rb

Additional Non-Standard Directories:

  • db/queue_migrate/ - migrations for Solid Queue
  • site/ - Hugo static site generator for documentation (completely outside Rails)

2. Model Organization - ANEMIC Models

Models are deliberately thin and focused on relationships/validations only:

Example models:

  • app/models/post.rb - 16 lines
  • app/models/account.rb - 52 lines
  • app/models/crosspost.rb - 34 lines

Models delegate behavior to service objects:

def platform
  PublishesCrosspost::MatchesPlatformApi.new.match(self)
end

No model concerns - both app/models/concerns and app/controllers/concerns are empty (just .keep files)


3. Controller Patterns - Service Object Delegation

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
end

Controller inheritance:

  • ApplicationController - base authentication
  • MembersController - authenticated users
  • AdminController - admin-only features
  • ApiController - API endpoints

4. Service Objects Pattern - EXTENSIVE Use

51 service object classes in app/lib/

Naming Convention:

  • Verb-based names: CreatesCrosspostForPost, PublishesCrosspost, FetchesFeed, DeletesUser
  • Present tense verbs (not "Create" but "Creates")

Organizational Patterns:

A. Standalone Service Objects:

  • creates_crosspost_for_post.rb
  • deletes_user.rb
  • checks_feed_now.rb

B. Namespaced Service Objects (Composed Objects):

Major service objects have nested namespaces with helper classes:

PublishesCrosspost (main class) with 9 supporting classes:

  • publishes_crosspost/applies_format_string.rb
  • publishes_crosspost/composes_crosspost_content.rb
  • publishes_crosspost/finishes_crosspost.rb
  • publishes_crosspost/matches_platform_api.rb
  • publishes_crosspost/munges_config.rb
  • publishes_crosspost/truncates_content.rb
  • etc.

FetchesFeed (main class) with 8 supporting classes:

  • fetches_feed/gets_http_url.rb
  • fetches_feed/parses_feed.rb
  • fetches_feed/creates_crossposts.rb
  • fetches_feed/persists_feed.rb
  • etc.

C. Result/Outcome Pattern:

Two result object classes for handling success/failure:

  • result.rb - returns data on success
  • outcome.rb - returns message on success

D. Configuration Objects:

  • crosspost_config.rb - Struct with 29 keyword arguments

5. Platform Abstraction - STRATEGY PATTERN

Base Platform Class:

app/lib/platforms/base.rb - Template method pattern

Platform Implementations:

  • Platforms::Bsky
  • Platforms::X
  • Platforms::Mastodon
  • Platforms::Threads
  • Platforms::Instagram
  • Platforms::Facebook
  • Platforms::Linkedin
  • Platforms::Youtube
  • Platforms::Test (test environment only)
  • Platforms::TestFailure (test environment only)
  • Platforms::TestSkipped (test environment only)
  • Platforms::Null (for testing)

Nested Platform Services:

Each platform has its own namespace with specialized services:

  • platforms/bsky/syndicates_bsky_post.rb
  • platforms/bsky/assembles_rich_text_facets.rb
  • platforms/youtube/uploads_youtube_video.rb
  • platforms/threads/creates_container.rb
  • etc.

Platform Matching:

publishes_crosspost/matches_platform_api.rb - Registry pattern


6. Dependencies and Architectural Choices

Custom Engine:

  • searls-auth - Authentication engine (mounted at /auth)

Background Jobs:

  • solid_queue - Job backend (Rails 8 default)
  • mission_control-jobs - Job monitoring UI

Platform Integration:

  • bskyrb, didkit - Bluesky
  • x - X/Twitter
  • mastodon-api - Mastodon
  • Direct HTTP for others (Threads, YouTube, etc.)

Feed Processing:

  • feedjira - RSS/Atom parsing
  • httparty - HTTP client
  • reverse_markdown, redcarpet - Markdown conversion

Notable Patterns:

  • Uses bcrypt directly instead of Devise
  • Uses propshaft instead of Sprockets
  • turbo-rails + stimulus-rails - Hotwire stack
  • tailwindcss-rails - CSS framework

7. Configuration and Initialization Patterns

Custom Initializers:

  • config/initializers/invariant_assumptions.rb - Runtime assertions about Rails internals
  • config/initializers/cors.rb - CORS configuration
  • config/initializers/feedjira_ext.rb - Extends external gem
  • config/initializers/ses_api_delivery.rb - Custom email delivery

Application-level configuration:

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

Custom Middleware:

  • Swaps ActionDispatch::Static with custom Middleware::ConditionalGetFileHandler
  • Inserts Middleware::NotifiesAdminsOnException before ActionDispatch::DebugExceptions

8. View Organization

Standard Rails view structure with domain-organized partials:

  • app/views/accounts/
  • app/views/posts/
  • app/views/crossposts/
  • app/views/shared/ - 26 shared partials
  • app/views/icons/ - 31 icon partials

Uses Turbo Frames/Streams extensively


9. Testing Strategy

101 test files using:

  • Minitest (default Rails)
  • mocktail - Mocking library (not Mocha/RSpec mocks)
  • capybara + capybara-playwright-driver - System tests
  • vcr + webmock - HTTP mocking
  • simplecov - Coverage

10. Domain-Driven Design Elements

Core Domain Concepts:

  • Feed - RSS/Atom source
  • Post - Individual entry from feed
  • Account - Social media platform account
  • Crosspost - Syndication of post to account
  • Notification - User notifications

Value Objects/Configuration Objects:

  • CrosspostConfig - 29-field configuration Struct
  • Result / Outcome - Operation result objects
  • Constants module - Domain constants

Rich Domain Logic in Services, Not Models

State Machine Pattern:

Crosspost status: ready → wip → published/failed/skipped

Job Orchestration:

  • CheckFeedsJob - Periodic feed checking
  • FetchFeedJob - Fetch individual feed
  • KickOffCrosspostsJob - Start crosspost workflow
  • PublishCrosspostJob - Publish to platform
  • FinishCrosspostJob - Finalize (for async platforms like YouTube)
  • RequeueAbandonedWipCrosspostsJob - Error recovery

Summary of Architectural Patterns

This Rails application demonstrates:

  1. Service Object Architecture - Heavy use of single-purpose service objects instead of fat models
  2. Strategy Pattern - Platform abstraction with polymorphic behavior
  3. Result/Outcome Pattern - Explicit success/failure handling
  4. Composition Over Inheritance - Service objects compose other service objects
  5. Template Method Pattern - Base platform class with overridable methods
  6. Registry Pattern - Platform matching system
  7. Anemic Domain Model - Deliberate choice to keep ActiveRecord models thin
  8. Command Pattern - Service objects act as commands
  9. State Machine - Crosspost workflow management
  10. Hexagonal Architecture Influence - Platform adapters isolate external dependencies

Key Difference from Standard Rails

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
@andyw8
Copy link
Author

andyw8 commented Dec 23, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment