In my recent RailsConf talk I said that I would help out with questions regarding component-based Rails applications (#cobra). A few days ago I got one such question via twitter: How to make a unified admin tool for independent engines?. Here is the gist:
Zac: @shageman starting an app from scratch using #cbra and wondering how to handle a shared admin tool w/ regards to non-shared dependencies
Stephan: @zachallett Are you wanting to build one admin for multiple independent parts?
Zac: @shageman that was my thought, I would like to mount subdomains as engines, and have a unified admin tool for the independent subdomain apps
Stephan: @zachallett Basically, you have two options: make one admin(engine) per engine or extract admirable stuff and have one admin engine.
This post adds detail to my answer on twitter.
Let me explain the scenario and then the two options I am referring to. For this post I am going to skip all the details of what a component-based Rails application is and how you build it. For that, please refer to my introductory blog post, my sample app, or one of my talks on the subject.
An example of independent Rails engines
Two engines being independent means that they do not require any of each others code. Imagine the following application structure:
Let the registration component be one where a customer can sign up for the service. It has a Web UI that guides through the sign-up process at the end of which the registration is recorded. Typically this creates a user record. Bear with me and imagine that the automatic creation of user records is not possible due to legal requirements demanding the confirmation of certain aspects of the registration. As part of this manual process customer support personnel create a user record in the service component. Only then can the service be used by the customer.
As you can see, the components are not dependent on each other. The databases could stand alone. The question now is: if we need to manage both registrations and users how do we design the administration pieces? I will discuss Option 2 from my tweet first as, in my experience, that is what feels more natural to many.
Obviously, one could argue that since there are no dependencies one should simply build two applications. While it is an option, I won’t get into that here. That is a discussion of its own.
Option 2: A single, separate admin engine
The reasoning behind creating a single administration component is that there ought to be one admin experience. As we discussed, the same people need to manage data in both sides of this application. If you add an admin engine, your app will look like this:
Unless we are willing to create multiple models for the same database table, this alone is not going to work: The admin engine needs access to the data in both original engines. So, unless we are willing to duplicate the models from those engines (which is really Option 3), we have to have the admin engine depend on the other engines in order for it to have access to their models. Those additional dependencies lead to this structure:
Since the admin engine can now require both the service and the registration component, it has access to the models and an admin interface can be written. However, I don’t think that is an ideal solution as the admin engine now has access not only to the models (which it needs), but also to controllers, helpers, views, and other files from these two engines (which it does not need).
I strongly believe that if something is not needed it should not be accessible. In order to fix this, we can split the service and registration components vertically: into a web and a data layer. That change results in the following structures:
I have seen this structure evolve for applications with as few as three components and with as many as thirty. It typically leads to vertically and horizontally sliced applications. In this example we went from two components to five. If you don’t like to see that many components, you might want to consider what was Option 1 in my tweet.
Option 1: One admin interface per component
The reasoning behind creating an admin section separately for every component is that the overall structure of the application can stay simpler. It makes every component self-contained: customer-facing code and admin code live next to each other and don’t depend on anything else. Because of this, in the first iteration, the structure looks like this:
Yep, it is exactly the same as the original #cobra design. That is because the admin interfaces are simply part of the component in which the data resides. Yes, it is that easy. Or rather, it can be.
Let’s be devil’s advocate myself and ask some pesky questions:
- What if the admin UIs are supposed to share Visual design?
- Authentication and authorization are the same between the two admin sections: do they have to be duplicated?
- It is one admin interface! Aren’t we creating a fragmented experience?
Shared visual design
If visual design is supposed to be shared between the admin interfaces, we can create a component with shared admin assets and depend on it in both application parts. That would make the application structure look like this:
Interestingly, we can make the same argument about these new dependencies as we did earlier about the dependency of the single admin engine onto the original components of the application. Parts of registration and service do not need to depend on the admin assets.
By now you probably know the trick: we separate out each admin section into its own component. From before we remember that in order for this to work we can either duplicate the models in the admin engines or extract the data parts of registration and service. The two versions of this extraction look like this:
How do we prevent code duplication if authentication and authorization are the same for all parts of the administration interface? The answer staying in the spirit of what we have done so far is to extract the common part. That leads to this structure.
On one of my previous projects we found an intersting alternative to this. Instead of a new component we used an initializer to add a middleware in front of Rails. This middleware enforced auth for access to admin sections. An unusual side-effect of this is that the engines themselves don’t contain anymore auth code. You will want your integration tests to be watertight to ensure that you don’t have unprotected admin features out there.
Preventing a fragmented admin experience
In any application where there are multiple admin components one is initially dealing with a fragmented admin experience. A component with common admin assets brings the pieces visually back together. But they are still independent. How does one get from one to the other?
A solution to this is to inject the entry points (URLs) of all admin components from the Rails app into all admin components. This way every admin component can render links to all other admin components. In combination with common admin assets, a user of this application will not be able to tell that they are being sent from one component to another when they are navigating through different parts of the admin interface.
Generally, there are at least three options to dealing with admin components in Component-based Rails applications:
- A single admin component depending on the data layers of all necessary parts
- An admin engine per component, either within or outside of the component itself
- Independent admin engine that duplicates necessary parts of the model layer (not discussed in detail)
But did I really answer the question of how a Component-based Rails application should be structured when it seems to contain independent parts? Nope, I didn’t. Which solution you should go for is dependent on where you are with your application and how far you want to take things.
One very common observation when working with #cobras should have become obvious:
If you extract one component out of your application, chances are you are going to extract at least two: the component you wanted to extract and a component for the stuff that is shared between the original app and the new component.
In essence, #cobra design is about the discovery of your domain and the application you are building. Zac could have never asked his question about structure his app in his case and I could have never come up with a handful of ways to do it, had it not been for the tool that is #cobra. We would just write a ball-of-mud Rails application and never get to ask or answer any of these questions. Because we would never be able to see them. I am convinced that if you start thinking about your (Rails) applications in terms of components, you will understand your own domain better and will ultimately start writing better applications.
All the graphs in this post were created using cobradeps, a gem to print the dependencies within component-based Rails applications. The application skeletons that are the basis for the different structures discussed can be found on github.
I am writing a book on #cobra: Component-based Rails Applications. I would love to hear your feedback regarding the subject, what you would like to see in the book, and any other comments you might have.