Talks
In a World of Middleware, Who Needs Monolithic Applications
Summarized using AI

In a World of Middleware, Who Needs Monolithic Applications

by Jon Crosby

In this presentation titled "In a World of Middleware, Who Needs Monolithic Applications," Jon Crosby explores the evolution and benefits of using Rack middleware within the Ruby ecosystem, particularly in contrast to monolithic applications. He emphasizes the importance of choice in the current Ruby landscape, highlighting how new abstractions allow developers to create more modular applications. Key points discussed include:

  • Introduction to Middleware: Crosby begins by introducing Rack middleware, advocating against monolithic applications due to their drawbacks. He expresses the idea that recent advancements in Ruby invite more flexible and manageable solutions.
  • The State of Application Development: He discusses the historical challenges of building applications using older technologies like CGI, portraying a familiar situation where applications were tightly coupled and difficult to maintain. Modern frameworks, especially Rails, have introduced better abstraction patterns that separate concerns effectively.
  • Middleware Explained: Middleware acts as an intermediary in the application stack. Crosby illustrates this with a simple middleware example where functionality can be added or modified without altering the core application. This modularity enhances maintainability and facilitates features like authentication, logging, and error handling.
  • Examples of Middleware: Various middleware examples are presented such as Rack Profiler for performance monitoring, Rack::ShowExceptions for error handling, and Rack Cache for efficient caching mechanisms. These illustrate how middleware can be leveraged to enhance applications.
  • Cooperative Middleware: Crosby highlights the concept of
00:00:11.799 This talk is about Rack middleware. To set the mood appropriately, I would like to point out that a great abbreviation for middleware is actually the combination of a lowercase 'w' and a lowercase 'M,' which is akin to The Rock's sign. We should also start by addressing the somewhat silly title of this presentation: 'In a World of Middleware, Who Needs Monolithic Applications?' Obviously, no one needs a monolithic app; that's quite detrimental. The real key is choice, and we have more of that in the Ruby world today than we've had in the past. The more we learn, the more kinds of cool abstractions we can build. I believe our community does a good job of pacing that correctly. We do not build our architecture prematurely, and Rack is a very good example of this. I hope to share some of those insights with you today.
00:01:03.000 As a brief introduction, my name is Jon Crosby. If you'd like to get in touch with me, you can use my website. I am the author of CloudKit, which you can also check out on my site. Additionally, I am a contributor to the Rack project on GitHub. Both projects that I've mentioned contain examples relevant to our discussions today. Recently, I became an employee of Engine Yard, specifically working on our new Solo offering, which is an on-demand platform for Rails applications in the cloud. This platform actually goes beyond Rails applications, as you can use any Rack apps or basically anything you want on your own instance.
00:01:36.320 You have a high degree of control and automation on this platform. I do come bearing gifts today as well. You may have already read about it on the blog, but during the entire conference and the hackfest, we are offering the Solo platform for free. If you haven't received a postcard yet, find one of the Engine Yard staff, and we'll give you a card with a code to use the platform for free during the conference. If you choose to sign up for a paid account this evening at the hackfest, we can provide you with the cards.
00:02:00.200 If you decide to sign up for a paid account before the end of the month, we'll also give you a discount on the sign-up. Now, getting right into the subject matter, do you remember CGI? More specifically, do you remember building something like this app using CGI? I will admit to having built something like this in a previous life. I want to show you an excellent example – but first, a health warning: the following slide contains some Perl code, and not just any kind of Perl, but Old School Perl CGI. This example is particularly interesting because I found it online in a real tutorial teaching someone to write Perl using CGI.
00:02:27.040 Take a look at this code. We're checking for a parameter, and if you're a Rails user, you're likely already cringing because you're familiar with concepts like REST and having nouns in your URLs. We see this action parameter, as if the HTTP methods aren't good enough. We're checking for some arbitrary string, and if we find that, we're building up some SQL and using a low-level SQL library. If we find any rows, we build up our table structure right here, incorporating styling within the same bit of code. This illustrates how some of us learned to write apps while feeling our way around. Perl was actually the first language I used, and I wrote a mess like this at one time, which is what we call a monolith. Monoliths are bad.
00:03:39.600 Nowadays, we have things much better, of course. We have Rails, which provides a clean separation between models, views, and controllers. It goes beyond that, incorporating nice abstractions like Active Record for callbacks. Rails is certainly not the only framework available; there are many other great frameworks providing elegant abstractions. However, as we build these types of applications, it becomes evident that we are often cramming things into places that could be abstracted in new ways, and with every new project, we learn more about this.
00:04:14.000 Authentication is a clear example of a feature that could be pulled out. Specifically, single sign-on is a great candidate for abstraction. Caching is another aspect that we might consider extracting from our applications. There is a valid reason for doing partial page caching within an app when you need to access specific component logic on your page. However, core screen caching, where you're caching documents with ETags and last-modified timestamps, can certainly be extracted from our apps.
00:05:09.880 Let’s take a look at the current state of affairs with an authentication example using OpenID and OAuth, which I hope to see more implemented in the wild. If I’m running a Rails app, which could be any kind of app, I would typically have to install two different plugins—one for OpenID and one for OAuth. I will need to generate some controllers for handling sessions, along with generating models for users and potentially user roles. This entails migrations that may need to be executed.
00:05:24.680 If I install plugin number two, I may need to modify the session controller generated for plugin number one. In the worst case, I might end up monkey-patching the Rails stack in a way I'll regret later. This is a sad state of affairs. We can learn from the architecture of the web itself. HTTP comes in at the front of our stack, goes through zero or more intermediaries, which could be proxies or caching servers, and finally reaches the application at the end of the chain. Most of our time is spent working in this last application section, and this is where Rack comes into play.
00:06:00.400 The architecture of Rack will look very familiar. A Rack app receives HTTP at the front of the stack, passes through zero or more intermediaries (middleware), and finally reaches the app at the end. We can conclude that Rack is the web, and inversely, the web is Rack. For those interested, Rack is based on WSGI, a standard that originated from the Python community. WSGI is more detailed and possibly more rigid than Rack. It stands for the Web Server Gateway Interface and aims to provide a lean abstraction layer between web frameworks and web servers.
00:07:04.120 The goal was to prevent every framework from having to write a new adapter each time a new server emerged, like Thin or Mongrel. Rack took this concept, simplified it, and ran with it, leading to the development of the Rack specification, which has no acronym. Here’s a basic Rack app: Rack apps are Ruby objects that respond to the 'call' method. For example, a lambda will suffice. They take exactly one argument, which is the environment that we will explore in detail shortly. They are required to return an array of exactly three items: the first is a status code (for instance, we use '200' for HTTP success), the second is a hash containing key-value pairs mapping to the headers you want to return in your response, and lastly, the response body, which must respond to 'each'.
00:07:56.600 To run this Rack app, we simply type 'run.' We can save this into a file called config.ru, which stands for rackup. We use Rack's built-in utility called rackup to run this file. If we hit this with curl using Rack's default port, we’ll see that we receive the expected response. You don't necessarily need to use lambdas to write your apps; in fact, as your apps grow, you probably won't. You can simply define a class with a 'call' method, and that will accomplish the same task. You can generate all the required details about the environment by running rake spec if you've cloned the Rack project.
00:09:04.960 When you run rackup in development mode, 'rack lint' is included as middleware that will check your app for compliance with the specification. It is highly advisable to run 'rack lint' consistently throughout your development process. Rack's specification includes many items of interest, such as the request method. If I access this environment hash within my app for the request method, I would return the expected HTTP method, whatever that is. Path info will provide the path to whatever they are requesting. There are also HTTP-style header parameters; for example, if someone sets an accept header, it will come in formatted accordingly.
00:10:06.480 Rack environments also include namespaced protected keys. If you're building your own app, you should avoid using these keys for safety and compliance with the current specification. Among the most pertinent is probably 'rack.input,' which is the input stream itself. If I send a JSON object to my app, rather than it being a parameter, it's a POST containing some JSON. I can use 'rack.input' to respond to reads and rewinds. If you'd like to be more organized, you can create your own namespaces within the environment. This approach is beneficial if your apps need to interact, enabling you to include items in the environment that cannot be spoofed externally.
00:11:16.560 For example, if you attempt to set something like 'X-Remote-User' as an environment variable that you trust downstream, I could easily spoof that in the browser. If I set that header in Firefox or a tool like Firebug, 'rack' will pass that straight on through. Consequently, if you require a method to verify that something originated from an internal source you created, you can place it in your own namespace and insert it into the environment. If the raw hash format isn't to your liking, you'll likely outgrow it eventually, so you can wrap the environment in a request object to retrieve more familiar attributes like 'params' and headers.
00:12:34.880 Now, let’s address middleware. Middleware is everything in front of our app, as we've already seen in the previous diagrams. To set this particular configuration in a rackup file, we simply say, 'use middleware a, b, and c,' stacking them up in front of the app, and then we run the app at the end. What we have here is the world's simplest piece of middleware, named 'Go Slower.' To illustrate how middleware functions: when Rack starts up, it calls 'rackup' and initializes this middleware while passing in the app. Although the app may refer to an endpoint in the stack, consider it the next item in the hierarchy.
00:13:52.360 On each request, Rack will invoke your middleware while passing in the environment. In this middleware's case, we deliberately sleep for one second because our app is running too efficiently. Afterward, we call down to the next layer in the stack. The central power of middleware is encapsulated in this line: we return the result of 'app.call.' This returns everything that is generated by the downstream app, allowing us access to modify the environment both prior to and after that call. Essentially, any actions performed in your middleware will occur around this pivotal juncture.
00:14:57.800 There are many examples available that illustrate people creating self-contained middleware pieces within the Rack and Trip project. Just to highlight a few, there’s one called Rack Profiler, which you can drop into a Rack app for profiling. There's also 'Rack::ShowExceptions,' which is frequently implemented in plugins, but in using Rack, can catch any exception thrown and provide you with an email that contains the call stack and the exception itself. Moreover, Rack offers a way to use JSON-P, allowing you to manage padded data entry and removals in and out of your application simply by placing a piece of middleware at the front.
00:16:18.440 Recently, we've added 'Rack::CSSHTTPRequest,' which lets you request read-on data from another web service by encoding it as CSS while embedding an array of CSS classes containing the requested data elements. This feature is certainly interesting, so I encourage you to check it out. Beyond the Rack and Trip project, this might be significant enough to stand alone—it's called 'Rack Cache.' I highly recommend looking into how that works as an effective prelude to infrastructure investments in caching systems like Varnish.
00:17:41.000 Finally, there's 'Rack Not Found,' which I am guilty of writing. It does what you expect—it simply returns a 404 response. While this might seem trivial, consider the structure we have with our stack that calls through multiple middleware instances until it hits the app at the end. If applications are built this way, with middleware that collects specific architectures, we would be faced with instances of uncertainty. Here’s where the concept of 'Cooperative Middleware' comes into play. Cooperative Middleware examines the URI space: if your application is built in a standard manner using Rails, for example, your entire URI space might be utilized when you’re defining routes.
00:18:57.680 At this juncture, 'Cooperative Middleware' only targets the space that your application requires. As an example, CloudKit is an open web JSON appliance allowing you to expose resources directly as RESTful endpoints without the traditional overhead of defining all the auxiliary infrastructure. In a rackup file, I could declare specific endpoints such as notes and to-dos, automatically generating a RESTful resource collection that supports all transactions that you would expect in a real REST application. CloudKit can effectively store everything in the backend using Tokyo Cabinet, which is notably rapid. If you change the word 'expose' to 'contain,' an authentication filter will be introduced that cooperates well with OAuth and OpenID.
00:20:41.840 This design means it can elegantly build a session pool and drop the specified filters in front of the service. If it finds itself situated at the end of the stack, it will either delegate requests or drop its own 404 app, with the development mode bringing up a developer page. The OAuth filter works under the principle of claiming only what URI space is actually needed. It will review everything making its way under OAuth, while the OpenID filter follows similar rules and interacts with expected behaviors listed in OpenID specifications. Any requests that do not match these criteria will be passed downstream, but this raises some new questions about how these components can cooperate.
00:21:33.680 Consider a browser accessing the CloudKit service. It will trigger the OAuth filter first, failing due to browsers lacking support for OAuth currently. However, it also supports a draft spec called OAuth Discovery, allowing it to create challenge headers for authenticating clients. If the request is rendered on the browser, the OpenID filter would also fail, resulting in a login page with those challenge headers presented. Unlike basic authentication, which tends to trigger an unsightly pop-up, the browser merely ignores it. If the browser fails on OAuth, it can attempt OpenID login, and once successfully logged in, the request can proceed to the intended service.
00:23:01.840 In other scenarios, such as web services communicating with each other using OAuth for signing requests or desktop apps doing the same, they would also trigger the OAuth filter, experiencing similar failures. But since they do not concern themselves with the display of a web login page, they recognize the challenge headers and can respond by completing the OAuth dance. After successful negotiation, they can return the valid request token to the server which can then deliver the desired resource. This discourse demands that middleware announces its presence in the stack.
00:24:43.640 Long ago, this issue was addressed through the use of HTTP. If we have a stack of proxies in front of a web app, they can declare themselves via specific headers. For instance, if we had a proxy server called 'Ricky' supporting HTTP/1 and others like 'Ethel' and 'Fred,' they would show up in our application via the 'via' header. While effective for debugging, we can utilize this concept for Rack middleware, though it is not advisable to literally use headers since we shouldn’t step outside HTTP protocol boundaries. Instead, CloudKit is configured to maintain its own namespace, informing the environment that authentication is active for that app.
00:25:48.200 This way, the service is aware of needing to monitor users' access rights. A pseudo 'via' array is embedded in the app, signaling upstream that OAuth is active. The OpenID filter adapts its behavior based on the expected inputs. For authenticated users, the relevant user data is placed in the environment. Instead of traditional database IDs, as we utilize OpenID, we employ internet-derived IDs, which are URLs. Various alternative stacks can also be assembled. Rack includes 'Rack::Map,' which is especially intriguing. For example, when building a blog, you could create two Rack apps: one for public access and another for database administration.
00:26:45.960 I would advocate for separating administration from the public interface as it eliminates the need to handle user roles in the same stack and protects specific links directly. Instead of scattering authentication, you could simply place a middleware layer in front of the database admin app. Therefore, anything executed under the database administration route would be processed through that layer. Additionally, integrating 'Rack::Map' with Sinatra can create versatile combinations. For instance, if we leverage the latest version of Sinatra, we would gain access to the minimal routing options accompanied by rendering helpers and the capacity to easily include our Sinatra apps within the Rack stack.
00:27:53.600 There’s even 'Rack::Cascade,' though this technique is not very common, possibly due to performance concerns. This allows for building two applications: if you configure a cascade in your rack file, it will query each app to find one that produces a valid response. The first app that provides a response aside from a 404 code will route the request to the client. You can also customize these status codes as needed. Another intriguing application is using Sinatra within a Rails application stack. The middleware we built earlier can be seamlessly integrated using a configuration directive within the latest environment files, enabling Rails to pull it up automatically.
00:28:31.200 In the end, you’d have a Rails application with the same functionality you'd typically develop, but bolstered with the addition of this API, all neatly packed without generating additional controllers or models or anything like that, relying on Tokyo Cabinet at the backend. Reflecting on our architecture diagram where we may overlook the apps behind them, envision if Rails does not use the complete URI space and delegates requests somewhere downstream. In such a scenario, Rack might insert a 404 at the end, or Rails could do that if it does not identify any subsequent approaches.
00:29:18.800 This opens the doors to constructing applications built on the principle of middleware. So, with that, I conclude my presentation. Thank you all for your attention. I'm happy to open the floor for questions, although we might have to curtail some time to catch up after any delays.
00:29:57.080 Yes? So you are working globally. How do you detect collisions between multiple middleware that utilize the same URI? There isn’t a definitive solution for this yet. The question has not been addressed within the Rack specification, which is purposefully minimal. We are at version 0.9 of Rack currently, approaching 1.0, and this would be an opportune moment to come up with suggestions to include in that future specification.
00:30:06.560 Do we have any additional questions? I can't see the middle back as well because of the light. Please repeat it. As for authentication or authorization aspects of your application, sometimes there are sections of your app that might not require authentication. As you mentioned, the CloudKit has recently added a feature addressing that. Could you explain how you can selectively manage mappings in middleware? Sure! As far as I can recall, the question revolves around determining which parts of your app need authentication, particularly with regards to the new feature added to the CloudKit.
00:30:53.080 In CloudKit, when setting up your OpenID filter, you have the flexibility to provide it with options, such as a hash list that delineates URIs you wish to exempt from authentication. This approach allows for easy management of which specific resources or routes need authentication and which do not. I feel confident that there are more sophisticated implementations that will surface as Rails builds upon this middleware stack and as we develop more robust applications.
00:31:58.040 What you shared regarding the database blog example is also quite valid. Yes, you could certainly place middleware directly above it. In fact, employing an authentication layer just in front of the admin interface would certainly be the correct course of action. Any additional inquiries? Yehuda mentions that, beyond the straightforward mapping option we discussed, there are initiatives underway to enhance Rails and merge routing capabilities into a middleware system. In essence, it will enable routing within the middleware context itself.
00:32:47.020 Thank you very much for your engagement. I appreciate everyone's contribution to making this talk effective.
Explore all talks recorded at MountainWest RubyConf 2009
+3