Rack is the underlying web server interface used by popular Ruby frameworks like Rails and Sinatra. I previously wrote about making a Rack server from scratch after finding a lack of how-to help while googling. Recently, I found myself digging back in after becoming inspired by Rack’s middleware stack. There are plenty of guides floating around on how to use and create middleware for Rack, but I wanted to take the stack concept itself and use it in a completely different context – something that wasn’t necessarily a web application.
A middleware stack is not the traditional LIFO (last in, first out) data structure that comes to mind for many programmers when they hear the word “stack”. It’s a layered series of code modules, each of which modifies the state of an incoming data structure. After each layer has a turn, the resulting (new) structure is returned.
Here’s the situation which made me want to adapt this pattern:
- I have a collection of isolated code modules, each of which does a specific task.
- The sum result of those tasks are used to make decisions somewhere else in the program.
- The code modules can be assembled and used in various configurable combinations.
- Sometimes, the order they run in matters.
- I want to easily communicate the architecture to teammates in a way that is familiar to them.
There are only two types of pieces in this puzzle: The middleware and the builder.
A middleware is nothing more than a class that takes an “application” (more on that later) as its constructor argument, and which implements a single method,
call. This method takes one argument: A hash of the current “request” environment. In Rack parlance, the request is an incoming HTTP request. There is no HTTP here, so the “request” is really just whatever is making use of the middleware stack. The only requirement is that
call must return by passing the new environment (including whatever changes are made) to the next layer of the stack.
1 2 3 4 5 6 7 8 9 10
The builder is a class that manages a middleware stack and an associated application. There may be many builder instances depending on the number of desired middleware arrangements. The application is simply an object that responds to
call. In its simplest form, it is a lambda.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
Using the builder means defining a desired stack configuration and then calling it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
The results are hashes that have been manipulated in any number of ways by the various middleware layers.
A shortcoming I’ve identified is the difficulty of parallelizing stack layers that don’t absolutely have to run in a specified order. That’s a solvable problem, but worth noting when considering this pattern. One large benefit is the ease of communicating this architecture to my teammates, which I mentioned above as a primary goal. I can start a knowledge transfer conversation with “it works like Rack middleware”, and immediately establish a shared understanding. Would I use this pattern again? Maybe. It gets the job done and it’s fairly easy to understand. At the very least, I’ve emerged with a deeper understanding of Rack internals.
If you enjoyed this post, please consider subscribing.