Charlie Tarr

December 8, 2024

Building web apps with Ruby on Rails

I have spent more than a decade building web applications with Ruby on Rails

In the interests of adding value to a community, which has given so much to me, I would like to share some guiding principles, which I have come to live by. 

1. Prioritise customer experience

Whatever you build and however you build it, if your product fails to add real-world value to customers, which you're able to generate revenue from, you don't have a business, you have a hobby.

Developing an intimate understanding of your customers problems, pain points and frustrations is mission-critical.

Armed with this understanding, your challenge is to design a solution to their problem, which they will find incredibly simple, easy and intuitive to adopt.

This is the foundation of everything that follows. If you fail here, you will fail everywhere else and in all probability, waste a tremendous amount of time, energy and money in the process. I'm speaking from hard-earned experience here, with the aim of helping others to avoid my mistakes. 

When working through the process of understanding problems and considering the best way of solving them, you may benefit from embracing the Shape Up methodology

2. Work from first principles

Whenever you set out to solve a problem, you should ditch all your preconceived ideas and disregard the existing way of doing things.

It is essential that you start from a blank canvas and think from the ground up, about the best way of solving the problem at hand.

Nothing stunts truly original and innovative thinking, as much as being constrained by the way that things are currently done or have always been done. This is particularly true in larger organisations. 

3. Compress complexity

The true magic of engineering, is to be found in identifying the simplest and most elegant solution, to a complex problem.

I feel that Mark Twain did a good job of capturing this sentiment when he said “I apologise for such a long letter - I didn't have time to write a short one.”
 
Amongst other things, compressing complexity requires open-mindedness, patience, knowledge, experience, collaboration and wisdom. I would encourage you to be diligent and brave enough, to keep searching for the simplest and best solution, even when others have given up. 

The wonderful thing about the Ruby on Rails framework, is that compressing complexity is at the beating heart of everything the framework represents and strives for.

Having made the mistake of chasing shiny pennies and getting bogged down in the mud of complexity, I have come to truly appreciate the power and beauty of Rails, particularly with the advent of Hotwire, Propshaft, Kamal and Solid Queue etc.

In many respects, I had to go down the wrong road, by building a super complicated web application with API-only Rails, React and GraphQL, in order to appreciate to true magic of the majestic monolith

4. Embrace test driven development

One of the amazing things about Ruby on Rails, is how quickly you can get to "Hello World!"

In many respects though, this is like allowing someone who doesn't have a driving licence, to get behind the wheel of a supercharged Ferrari. If you don't know what you're doing, you can unwittingly create a huge mess, which could prove impossible to clean up down the road.

For the first few days, weeks and months, you can fully embrace the notion of "move fast and break things."

If you embrace test driven development though, you can introduce a healthy amount of friction to your build process and benefit from shrouding yourself in a safety blanket. 

With a robust test suite in place, it will be far easier to refactor your application, if you discover that you've gone down the wrong road, built up an unsustainable amount of technical debt and / or your development cycles grind to a halt because you've become bogged down in layers of complexity. 

Test driven development also provides a great framework for thinking critically about what you're trying to build, before you set about doing so. 
 
If you're not able to articulate what you're trying to achieve, in writing or in the form of a test, you shouldn't be building it at all. 

5. Make great design choices

As an opinionated framework, Ruby on Rails empowers developers to build applications at lightning speed, without really needing to understand what goes on behind the scenes.

This has it's benefits for sure, but to the uninformed, it also has its drawbacks.

Much like when solving a maths problem at school, getting the right answer is far less important, than being able to explain how you got there.

It's the ability to effectively reason your way through a problem, which truly equips you to tackle novel challenges, which you've never encountered before.

So it is with design. The goal is not to build every product or feature, in the shortest time possible. The goal is to design every product or feature, in a way that allows you to respond easily to changing requirements in the future, even if the engineers that originally wrote the code, are no longer involved with the project.

Great design is as much art, as science. One of the best books that I've ever read on the subject of design is Practical Object-Oriented Design In Ruby by Sandi Metz. 

Some of the key concepts and philosophies, which I took from her book include:
  • Developing a deep understanding and appreciation for the benefits of object oriented design, so that, with time and practice, you can instinctively recognise important relationships and boundaries that should exist in your applications
  • Adhering to the single-responsibility principle, in every part of your application, from individual methods to the classes that they live within and the bounded contexts, which you may choose to house those classes in
  • Really understanding and making effective use of public, protected and private scopes to encapsulate your methods and manage your dependencies effectively 
  • When and why to opt for composition over inheritance
  • How to avoid ever-expanding branches of conditional logic and to organise your code more effectively, by leveraging duck types

In addition to all the great lessons that are contained within Sandi's book, I would also recommend devoting some time to read and consider the key concepts contained in Domain-Driven Rails by the team at Arkency.

A lot of the concepts contained in the book are overkill, in the context of relatively small-scale applications. With that being said, there are some extremely valuable lessons to be learned, by considering how best to organise enterprise level applications.

From an architectural and design perspective, the challenges faced by enterprise level applications, are far greater than those faced by smaller ones. This same principle applies from a security perspective, for those applications, which are dealing with critical infrastructure e.g. banking or healthcare applications.

By learning about the design techniques employed by engineering teams that are tasked with building enterprise level or critical infrastructure applications, you will expose yourself to powerful ideas and concepts, which may one day prove relevant to the projects you're working on. This episode of the Ruby Rogues podcast is a great example of this.

One of the key lessons, which I took from Domain-Driven Rails, is the idea of not just organising your application vertically, by thinking in terms of MVC, but also horizontally, using bounded contexts. 

As a practical example, consider a situation where you have a User model and a method, which allows that user to pre-register for a new product. Thinking vertically, you could have:
  • A form in the View layer
  • A corresponding action in the Controller layer
  • A dedicated method in your Model layer
  • A specific column in the users table in your Database layer, which records that the user has pre-registered

That all makes sense from a vertical perspective, but where should the logic for processing this request be held? 

One approach might be to create an Authentication bounded context, in which you create two classes, namely PreRegisterJob and PreRegisterService, which are responsible for updating the processing the pre-registration request and updating the database. You might also create a Notifications bounded context, in which you could create jobs and classes for the purpose of sending relevant notifications to the user e.g. when they're able to complete their registration. 

Architected and designed effectively, you could then encapsulate all of this logic into the following method chains:

user.authentication.pre_register

user.notifications.confirm_pre_registration

Encapsulating related logic into bounded contexts has the dual benefit of organising your codebase more effectively and if it ever proved necessary in the future, of abstracting your code into separate applications or microservices.

On a final note, I would be remiss if I didn't tip my hat to Refactoring Ruby by Martin Fowler. A mind-expanding book on the best way of tidying up and better organising your code. 

Summary

I hope you find this article to be thought-provoking and helpful. 

I owe such a great deal to the Ruby on Rails framework and the broader community.

A massive shout out and thank you to DHH and the core team at Rails for creating and continuously improving this wonderful framework. 

About Charlie Tarr

Hey ... I'm Charlie, the Founder of Stacked and the Co-Founder of Empowered Wealth

Subscribe below to follow my thinking on wealth management, web development (specifically Ruby on Rails) and whatever else is on my mind. 

Thank you for reading.