A Deep Dive into Routing and Controller Dispatch in Rails

Alex Taylor
Mar 17, 2019 · 8 min read
Photo by Shah Safwan on Unsplash

Rails has a lot of magic that we often take for granted. A lot is going on the behind the clever, elegant abstractions that Rails provides us as users of the framework. And at a certain point, I find it’s useful to peek behind the curtain and see how things really work.

But opening the Rails source code can be absolutely daunting at first. It can feel like a jungle of abstractions and metaprogramming. A large part of this is due to the nature of object-oriented programming: by its nature, it’s not easy to follow a step-by-step path that would be taken at runtime. Sometimes, it helps to have a guide.

With this in mind, let’s take some time to explore how routing works in Rails. How does a web request accepted by Rack make it all the way to your Rails controller?

Familiar territory

For the purposes of this example, consider a Rails app with a single route and controller:

In this app, a request to will be routed to the . But how?


A lot of collaboration happens to get from request to controller, so it will be useful to get a bird’s eye view before diving in. Here’s a diagram that shows how routes defined in are registered within Rails at boot time. We’ll explore these classes in more detail shortly:

The objects involved in route configuration. As we’ll see, Journey is also a part of the framework, but it’s useful to think of it separately when considering its responsibilities.

And here’s the sequence of events that occurs when we make our request to :

How a request to /users makes its way through Rails and into our UsersController.


The file you know and love! From a Rails framework perspective, this is the public interface. Declare your routes in this file, and Rails will take care of figuring out how to route a request to the right controller.


I kind of lied when I said was the public interface. It’s really a DSL to the public interface. The is the actual class that acts as the entry point for route configuration in a Rails application. It’s most famous for the method, which we’ve just used in :

At runtime, is responsible for coordinating the entire operation that we’re about to dive into: it receives the incoming web request, and collaborates with the objects below to determine how the request makes it into our application code.


Once upon a time, was a standalone gem, before it was merged into ActionPack. It focuses on routes, and figuring out how to route an incoming request. It doesn’t know about Rails at all, nor does it care — give it a set of routes, then pass it a request, and it will route that request to the first route that matches.

How it performs the routing in an efficient way is fascinating, and there’s a great talk from Vaidehi Joshi that goes into detail on the internals of . I highly recommend it!

holds on to the routes that our Rails app knows about. delegates to it whenever a new route is registered at startup, whether that’s from , an engine, or a gem like Devise that defines its own routes.


If we think of like an array, then objects are the elements inside. In addition to the metadata you’d expect this object to hold on to, like the path of the route, it also holds a reference to , which will get invoked if that route is chosen to serve the request.

In this way, each is kind of like a tiny web app that responds to a single endpoint. It has no knowledge of other routes aside from its own, but it can guide our request in the right direction when the time comes.


Contrary to what you might think, the that lives inside of each object is not some reference to the controller. There’s one more level of indirection here, as a means of keeping Rails code separate from the routing logic that concerns itself with.

is a small class which is responsible for instantiating the controller and passing along our request, along with an empty response object. It’s invoked when a suitable route is identified for a request. It has no knowledge about how a request arrived on its doorstep, but it knows what to do when it sees our request: instantiate the and hand it our request. As we’ll see, it acts as an object factory for our controllers, removing the need for us to declare our controller classes anywhere outside of the classes themselves.

This might seem like an almost needless indirection, but it’s worthwhile considering that's position between routing logic and controller classes allows either to change without affecting the other.


knows nothing about requests. It knows about routes, and it will quickly and efficiently identify the correct one for the request. So in order to map an incoming request to a route, we need something that knows about a request and a route. Enter .

It’s that actually invokes the once a route has been found.


Hey, we know what this is already! Welcome home. 😌 Now let’s connect the dots.

Back to where it all began…

Let’s circle back to our routes file:

When Rails is booting, a new gets instantiated. It evaluates the contents of the routes file and builds up a .

Because is the source of truth for all available endpoints in our application, it’s also first in line to receive a request from the outside world, after passing through Rack and various middleware. That’s right, this humble class buried in is the Walmart greeter of our application, ready with a smile and a wave as soon as a request comes through the door.

In order for to accept the request after it’s travelled through Rack and any middleware, it needs to implement Rack’s interface, which is as simple as implementing :


Here we build a new request object. This will end up being a fresh instance of , populated from , which is the incoming hash that Rack serves us.

After doing some string gymnastics on the incoming path, we pass the request off to , which is an instance of . We pass it a request and ask it to serve that request.

In , we iterate through the routes that match the path in the request:


Pay special attention to this line:

req.path_parameters = set_params.merge parameters# `req.path_parameters` is now a hash that
# might look familiar:
{:controller=>"users", :action=>"index"}

Notice that we’re actually enriching the request object itself with metadata that’s returned from the method. This is quite subtle, but it’s how communicates with the rest of the system. Once it identifies a matching route for the request, it “stamps” that knowledge onto the request itself, so that subsequent objects that deal with the request (like ) know how to proceed. Foreshadowing!

Anyway, when a match is finally found, we ask the route’s app to serve the request, then return the familiar array from any Rack app of status, headers, and body.

The reason for all this indirection is separation of concerns. In theory, can function perfectly fine outside of a Rails application, and as a result it’s abstracted the concept of an “app” into anything that implements Rack’s interface.

It’s here that Rails comes back into the picture. As I mentioned before, each object behind is actually an instance of :


is our entry point back into Rails land. It knows that a request is served by a controller, and it knows that the way to talk to a Rails controller is to send it a method and pass along the action, the request object, and a fresh new object to write the response into.

Notice that in the method above, we punt the question of which class to use to the request itself. When our request was first born, it had no idea who should be handling its request; it was just a glorified hash with a ton of metadata coming from the outside world. But thankfully, it passed through 's hands, who imbued it with a few crucial pieces of data:

Armed with this knowledge, the request object itself is now in a position to answer the question, “which controller should serve my request?”

Here’s what that looks like in the object:


Buried deep in the Rails framework is a great example of the Factory Pattern at work. We want to automagically choose the right class to handle our incoming request, and we don’t want to hardcode a list of all of our controllers anywhere, because that would be a pain. Since we now have a string, “users”, that tells us which controller this request wants to go to, we can build up the official class name, , and use to turn that into the class constant. Along with help from , which ends up invoking the method above, we have a way of instantiating the right controller for the request at runtime.

This is also a great example of the Open/Closed principle. Since Rails makes the assumption that your controllers are going to be named a certain way, you’re free to define a new controller simply by creating a new class that follows the naming convention, and defining its matching route. At no point do you have to update some ungainly mapping of route -> controller, or even register your controller anywhere. It’s the adherence to this principle that powers the Rails mantra of convention over configuration.

Now we’re getting really close: a message has been sent to the ! Through a series of intermediary methods, we finally invoke the method on the controller:

It looks like a lot, but ultimately we’re just using Ruby’s method to invoke the correct action on our controller instance. Simplified, it might look something like this:

UsersController.new(request, response).send(:index)

Unwinding the Abstraction

We just looked at a lot of objects. It can be hard to follow the path of execution when we need to bounce around so many different files. As a reminder, here’s the sequence of events again:

Another way to help clarify our understanding could be to reduce all of these steps down to a single method. Stripping away some of the abstraction, it might end up looking something like this:

A hypothetical and more procedural view of what’s going on, for illustration purposes


Photo by Dmitrii Vaccinium on Unsplash

If you made it this far, congratulations! 🎉 As you can see, there’s a lot going on behind the scenes, but hopefully this has helped to demystify some of the magic and appreciate the object-oriented principles at work.

Next time you add a new controller to your Rails app, sit back and appreciate just how much heavy lifting Rails is doing to take care of the details.

If you want to explore this code further, run from your Rails app’s directory and have a look at the classes we’ve explored, or check out the code on GitHub. Have fun!

Ruby Inside

Ruby articles and posts