Ruby on Rails in großen Anwendungen: Warum die Standardstruktur nicht skaliert – und wie wir das bei mindmatters lösen

Portrait von Jorgen

Jorgen

23. Juli 2025

Ruby on Rails ist für viele Entwickler*innen die erste Wahl, wenn es darum geht, schnell funktionale Webanwendungen zu bauen. Auch bei mindmatters setzen wir das Framework in der individuellen Softwareentwicklung gerne ein. Rails bringt ein mächtiges MVC-Pattern, eine große Community und eine enorme Entwicklungsgeschwindigkeit mit sich.

Doch Rails hat auch seine Tücken – vor allem, wenn Projekte wachsen. Die standardmäßige Ordnerstruktur von Rails skaliert schlecht, weil sie eine Trennung nach Art statt Funktionalität erzwingt: Alle Controller liegen zusammen, alle Models liegen zusammen, alle Services liegen zusammen. Das widerspricht einem wichtigen Prinzip der Softwareentwicklung:

„Things that change together should be together.“

Grafik zur Symbolisierung von Modularität, klare Strukturierung und Skalierung von Ruby-on-Rails-Anwendungen

Das Problem mit der Rails-Standardstruktur

Bei großen Anwendungen fällt schnell auf, dass die Standardstruktur von Rails für kleine Projekte optimiert ist – aber nicht für langfristig wachsende Systeme. Ein typisches Beispiel:

app/controllers/cart_controller.rb

app/controllers/products_controller.rb

app/controllers/profile_controller.rb



app/models/cart.rb

app/models/product.rb

app/models/profile.rb



app/services/cart_service.rb

app/services/products_service.rb

app/services/profile_service.rb



spec/controllers/cart_controller_spec.rb

spec/controllers/products_controller_spec.rb

spec/controllers/profile_controller_spec.rb



spec/models/cart_spec.rb

spec/models/product_spec.rb

spec/models/profile_spec.rb



spec/services/cart_service_spec.rb

spec/services/products_service_spec.rb

spec/services/profile_service_spec.rb

Die Dateiablage orientiert sich an der Art der Datei (Controller, Model, Service) – aber nicht an der Funktionalität oder am Bounded Context. Das führt dazu, dass zusammengehörige Dateien in völlig unterschiedlichen Verzeichnissen liegen. Wer an einem Feature arbeitet, muss ständig zwischen weit entfernten Ordnern navigieren.

Eine naheliegende Lösung wäre der Einsatz von Rails Engines, um eigenständige Module zu kapseln. Doch Engines sind schwergewichtig, erfordern es, eigene Abhängigkeiten zu verwalten, und eignen sich eher für vollständig gekapselte Subsysteme als für eine saubere Strukturierung innerhalb einer Anwendung.

Unsere Lösung: Ein Modules-Verzeichnis für Feature-basierte Strukturierung

Um unsere Rails-Anwendungen skalierbarer zu gestalten, haben wir bei mindmatters ein modules/-Verzeichnis eingeführt. Hier ordnen wir die Codebasis nicht nach Typen, sondern nach Features oder Bounded Contexts.

Statt Dateien nach ihrer Funktion zu gruppieren, legen wir alle zusammengehörigen Dateien an einem Ort ab:

modules/shopping/cart/cart.rb

modules/shopping/cart/cart_spec.rb

modules/shopping/cart/cart_controller.rb

modules/shopping/cart/cart_controller_spec.rb

modules/shopping/cart/carts_service.rb

modules/shopping/cart/carts_service_spec.rb



modules/shopping/product/product.rb

modules/shopping/product/product_spec.rb

modules/shopping/product/products_controller.rb

modules/shopping/product/products_controller_spec.rb

modules/shopping/product/products_service.rb

modules/shopping/product/products_service_spec.rb



modules/users/profile.rb

modules/users/profile_spec.rb

modules/users/profile_controller.rb

modules/users/profile_controller_spec.rb

modules/users/profile_service.rb

modules/users/profile_service_spec.rb

Was bringt das?

  • Logische Gruppierung nach Bounded Contexts: Alle Dateien, die zum Cart-Feature gehören, liegen zusammen. Das Gleiche gilt für Products und Users.
  • Bessere Auffindbarkeit: Wer an einem Feature arbeitet, findet alle relevanten Dateien in einem Verzeichnis.
  • Leichtere Wartung: Änderungen innerhalb eines Features sind auf einen Bereich begrenzt – das verringert Seiteneffekte und verbessert die Lesbarkeit.
Rails in großen Anwendungen

Rails für die neue Struktur konfigurieren

Damit Rails unser modules/-Verzeichnis erkennt, müssen wir es in der config/application.rb registrieren:

modules = root.join('modules')

config.autoload_paths << modules

config.eager_load_paths << modules

Tests sollten vom Autoloader ignoriert werden:

Rails.autoloaders.main.ignore(Rails.root.join('modules/**/*_spec.rb'))

Und schon funktioniert das Laden der Module nahtlos. Tests laufen weiterhin mit:

bundle exec rspec .

FactoryBot und Third-Party-Tools anpassen

Viele Bibliotheken, die mit Rails arbeiten, sind auf die Standardstruktur optimiert – aber lassen sich leicht anpassen. Als Beispiel nehmen wir hier FactoryBot. Anstatt Dateien in einem factories/-Verzeichnis zu speichern, speichern wir diese in _factories.rb*-Dateien und binden sie über eine Initializer-Konfiguration ein:

if Rails.env.local?

Rails.application.config.factory_bot.definition_file_paths += ['modules/**/*_factories.rb']

Rails.root.glob('modules/**/*_factories.rb').each { |file| require file }

end

Auch hier muss der Autoloader angepasst werden, um die Factory-Dateien auszunehmen:

Rails.autoloaders.main.ignore(Rails.root.join('modules/**/*_factories.rb'))

Andere Third-Party-Tools sind in unserer Erfahrung ähnlich leicht anzupassen.

Modularisierung zwingt zu klareren Namensräumen

Ein weiterer Vorteil unserer Modularisierung ist die Zwangstrennung durch Namensräume. Rails neigt dazu, einen globalen Namespace zu verwenden. Ein häufiges Beispiel:

Standard-Rails:

class User < ApplicationRecord

end

Problem: Jede Rails-App hat eine User-Klasse. Aber bedeutet User jetzt den eingeloggten Benutzer? Oder vielleicht auch eine Person, die mit Daten arbeitet, die auch ein eingeloggter Benutzer sein kann - aber nicht muss? Häufig wird dann unbewusst die User-Klasse auf alle möglichen Personen ausgedehnt, die eigentlich gar nichts mit den Benutzern des Systems selbst zu tun haben.

Die Modularisierung macht die Trennung explizit:

module Authentication

class User < ApplicationRecord

end

end



module Billing

class User < ApplicationRecord

end

end

Diese Namensräume sorgen dafür, dass es explizit wird, welcher Typ User gemeint ist – und verhindern, dass Modelle unbewusst über verschiedene Bounded Contexts hinweg verwendet werden.

Symbolische Grafik zur Strukturierung und Modularisierung von Rails.

Fazit: Warum sich diese Struktur für große Rails-Projekte lohnt

Die Rails-Standardstruktur ist für kleine Projekte ideal – aber für größere Anwendungen problematisch. Unsere feature-orientierte Strukturierung mit einem modules/-Verzeichnis sorgt für:

Bessere Wartbarkeit: Entwickler*innen arbeiten in klar abgegrenzten Modulen statt in vielen verteilten Verzeichnissen.

Saubere Namensräume: Der globale Namespace wird reduziert, was Missverständnisse und ungewollte Abhängigkeiten verhindert.

Natürlich ist dieser Ansatz ein Umdenken für viele Rails-Entwickler*innen – aber langfristig zahlt er sich aus. Gerade für größere Anwendungen in der individuellen Softwareentwicklung hat sich dieses Muster bei uns bewährt.

Du willst Dein Projekt mit mindmatters realisieren?

Werde Teil unseres Teams