Five foundations for building complex Rails apps

I recently wrote that for most apps out there, following the Rails-way or Vanilla Rails approach is more than enough. However, over the last few years, I have seen that a different approach is needed when the app is growing beyond the typical Rails app complexity (usually more than 100-150k lines of code).

To build maintainable complex Rails apps that are able to reflect the business itself and grow at the same pace as the business is growing, you need to establish a brand new foundation, both technical and mental.

Fix foundations for complex Rails apps

When you work with complex applications, there is no silver bullet to ensure they remain maintainable. There are layers, and not every layer will work in your own unique case. However, based on my own experience working with Rails over the last 16 years and my colleagues' experience in the business, I distilled the layers that have the greatest positive impact.

Domain Driven Design

The order is not random, as I think that strictly following DDD completely changes the way you write Rails applications for the better. There is no point in software existing if it does not solve real-world business problems, yet we often forget that. We create abstractions that make our work harder: naming, communication, maintaining what we have already created, and extending it.

The DDD philosophy shifts the way we work by prioritizing understanding of the business over everything else. I believe this is what distinguishes software engineers from software developers. In the past, I focused more on writing code and less on the business side and how it works. The Jira tickets were a source of truth for me, and I know that many people felt the same way.

Studying theory about Domain Driven Design is one thing, but having the actual conversation with different stakeholders and witnessing how the business operates in day-to-day operations is what shifts your perspective and makes code creation a more intuitive, purposeful, and interesting process. I will dedicate the whole next chapter to discussing that.

Mutation Testing

A technique that has been with us for decades, yet is not widely known or adopted among Ruby developers. Mutation testing is almost a bulletproof layer for the most complex logic you have in your app. In a shortcut, it verifies the quality of your tests.

Like DDD, it forces you to think about your code differently. It also helps you verify whether the tests you or the AI created are actually doing what they should.

Yes, mutation testing is expensive in terms of the time it takes to run the tests, but it’s not something that you do for all your tests in the codebase. This might sound confusing if it’s the first time you've heard about this approach, but trust me, it’s worth exploring. Companies like Google are using mutation testing for their most critical systems. And we are lucky because in Ruby we have a perfect tool for that, a library called Mutant.

Event Sourcing

When working with Rails, I got used to storing only the current state in the database. When an audit log was needed, there was usually a gem for that. It used to work really well in most cases. It worked unless I had to deal with complex business workflows with many state transitions.

A complex business workflow is where the event-sourcing approach shines most. In combination with Domain-Driven Design, it made it possible to map business processes to the code level without creating weird abstractions.

What is also great about Event Sourcing is that it’s not an all-or-nothing decision. You can implement it incrementally, only where it makes sense in the app. It has a higher entry threshold, but it does not conflict with Rails. And we also have a perfect tool for that, Rails Event Store.

CQRS

CQRS stands for Command Query Responsibility Segregation. It’s a more complex pattern that helps you separate the data writing process from the querying process. In a typical app, the model does everything. Still, when you need to show the same data in multiple places (pages, dashboards, reports, etc.) in different shapes, it becomes harder from a maintenance and performance standpoint when you follow the default Rails approach.

In combination with Event Sourcing, you create events and then, based on them, you create read models - small and specialized ActiveRecord models that are responsible only for the given page. Using this approach requires a change in mental model for creating Rails apps, but it significantly reduces the complexity and makes the maintenance easier.

Like Event Sourcing, CQRS isn't an all-or-nothing decision, and it’s well-suited to specific use cases.

AI

All the things mentioned above, DDD, Mutation Testing, Event Sourcing, and CQRS, help AI to write better code, the same way as they help humans. They provide explicit boundaries, named operations, and shrink the reasoning context. AI is already the foundation of our work as software engineers, so I’m not asking if we should use it. I’m exploring how to create AI-native environments.

This is the end of the getting-started chapter; the next article will open the Stepping Into Domain-Driven Design section, which marks a mental shift in how we perceive the software creation process.