Many years ago there was a Rails app. It started with things. These things were actually blueprints for other things. The other things needed many associated parts, and parts of parts. How many? The blueprints knew. The blueprints absolutely had to have an admin interface, but changing the blueprints would cause a chain reaction on things and parts. Every modification to the things and their blueprints permeated throughout the coupled network of various models. The admin UI complexity quickly skyrocketed as parts continued to branch out into more entities. It got to the point where blueprints had to have serializable, persistable snippets of logic. At that point every feature has become subject to a very difficult implementation, and thus the app degraded into the state of utter unmaintainability. It felt as if there was a content management system standing in the way of getting things done, imposing itself as the middle man between the feature and its implementation. It was like the system actually forced all the business logic to be reframed in terms of this higher level of abstraction.
The worst part? This was a minimal viable product for a newly-born startup.
The programmer’s nature encourages us to indulge ourselves in solving puzzles and modelling abstract concepts. It’s the passion that makes us lose sight of the danger looming ahead, the trap we’re edging towards thanks to our subjective assumptions and vague speculation, the trap of building a overdesigned and overcomplicated system for its own sake. A CMS trap. We suffer various consequences, ranging from burnouts, and loss of enthusiasm, to missed deadlines, and failed businesses, yet we never seem to speak of this mistake directly. Somewhere by a water cooler, an experienced colleague casually points out that you might be overcomplicating things. Somewhere in an IRC chat you get ridiculed for asking questions about a complex object model for a project that will most likely never see the light of day. Yet nobody can clearly explain exactly what is the underlying thought process. These casual remarks is all the education we get on the subject, and people end up learning this the hard way. That’s why I’d like to shine some light on this phenomenon. To start, here is my best shot at defining the CMS trap the way I see it.
A CMS Trap is a state of a web-application in which the development of content management systems is obstructing the development of the content.
If you are building a startup like me, you should know that this trap is especially dangerous in the early stage. Only a small percentage of companies get to play the long game, and by that time their problems have shifted onto a entirely different plane of existence. While these companies may also be subject to falling into the CMS trap, they would probably be able to afford it, if not even pursue this direction intentionally. Here, I’d like to focus on the much more abundant variety: the small companies. The problem would become apparent as soon as you’ve opened your doors to an influx of customers, who’d begin using your project, and providing you with real analytics and feedback. At this point your project would no longer be driven by your gut feeling, rather you’d have real data suggesting how to proceed, dictating which features to implement next. This would be the time when all of your initial architectural assumptions are being tested, and reality is beginning to set in. Reality has no tact, it doesn’t spare you any painful truths when dawning upon your hopeful application design. You’d wish you could refactor, but it’d be too late, as you’d be forced to keep up with new features instead, and implementing them would only be getting harder in this downwards spiral of dwindling productivity.
“Most of our assumptions have outlived their uselessness.”
— Marshall McLuhan
Simply put, we love designing systems. As soon as we form some understanding of a problem, we rush to our
/(?:whiteboards|moleskines|mindmaps|editors)/ and start passionately defining entities and their interactions. It feels good, it’s what we do best. We tackle some of the most fundamental decisions about the project. Then, having carefully outlined our assumptions, we commit to them. We like to think that we sow wisdom and flexibility with our early decisions, and we will thank ourselves later. With all of those useful points of extension and well-represented entities, what could possibly go wrong? The reality is, that most likely these early assumptions will restrict our future, not expand it. The day comes when we meet our old friend, the innocent “past self” staring back at us from the editor, smiling proudly. This well-meaning person spent hours, days, and weeks diverting our efforts into the abyss of speculative architecture, while having barely any idea about the real problems we’ll be facing. We are now stuck with all of that “helpful” code. It’s as if you decided to cook some salad, but instead of having separate ingredients laid out in front of you, all you have is another fully cooked salad given to you by a stranger, which you are now forced to dig through in hopes of fetching some of the pieces you need.
In programming, your past self is nothing but a stranger with boundary issues.
In the same spirit, imagine you come back to your computer only to find your app reorganized by some clueless stranger in ways that have little to do with reality. This isn’t very different to how we find ourselves looking at a system we’ve over-modelled in the past. Wouldn’t you wish that you didn’t have to deal with any of this garbage, and could instead simply greenfield your way ahead as dictated by your business needs?
To bring this back to my personal story, I eventually realized that with every new business feature I spent more time figuring out how to fit it into the existing framework I imposed on myself than actually designing the feature. As you may have guessed, I was thanking myself profusely for being so considerate.
“It’s harder to read code than to write it.”
— Joel Spolsky
Talking about architecture is a lot like talking about code itself. It’s not exactly that we can never be mindful of the future, it’s just that the odds are not in our favor. Code is easy to write and hard to change or remove. Every line we light-heartedly throw into the mix will eventually be taunting us with the timeless question: “guess what’s going to break if you touch me? ”. Architectural decisions, just like code, are easy to make and very hard to unmake. While in code this problem is alleviated with testing, in architecture we don’t have testing. The only measure of quality we have is really the measure of pain we feel when working on a new feature, and by that time it’s often too late. Bad architecture can suffocate your business even while your code is sporting 100% test coverage.
As with most traps, there is no specific way of knowing when you are walking into one. The best you can hope for is to have some sort of “tells” that warn you of an upcoming danger. Below I list some of these tells from my own experience. Seeing these things in your early stage project should at least make you suspicious.
“A state without the means of some change is without the means of its conservation.”
— Edmund Burke
Say you are faced with a new feature, and you find it to be a real yak shave. You realize that it will take a huge refactor, and you are arguing for ways to just avoid it. There is a fine line between negotiating feature requirements for reasons of efficiency, and negotiating them because you are stuck in the accidental buildup of legacy architecture. Could it be that your early speculative design decisions are starting to get in the way of today’s real business needs? Have you perhaps built too much too soon, and is it only a matter of time before the trap snaps, leaving your project effectively paralyzed? Don’t get me wrong, more often than not being conservative is a healthy defense against unnecessary complexity, it’s a standard practice of an experienced developer. The problem is when there is too much defense too early in the project’s life. It should definitely cause some suspicion.
“Hm, our pricing rules are different for various products, so we’ll need to find a way for an admin to define these rules in the admin panel. Maybe we should store code in the database and eval it?”
This is a classic sign of walking into the CMS trap. You are trying to come up with ways to let admin program some logic, which should then be saved to the database. If it wasn’t for the admin interface, it could’ve been done with only a few lines of code. However, now we are talking about creating price models associated with rules, and all the complexity emerging from this. Any further extensions to pricing capabilities, which could’ve been implemented with a line or two of code, would now have to take the shape of database migrations, forms, validations, and everything else down this rabbit hole. Do you really need admin UI for pricing rules at this point?
How do you implement 10 categories to place your products into? Typical answer involves creating a
Category model and then writing a script that will seed the 10 prescribed categories, which will be assigned to products. Then you’d make sure every developer runs this seed file. Also, don’t forget about running it in production of course. On every deploy. And on every pull. And when setting up a new machine. And when running tests. And naturally, if you change something in the seed file.
If your early-stage application relies on a lot of seed data, you are on a slippery slope. Things that can be assumed constant mustn’t need to be modelled as database-backed entities at this point yet, but I’ll get back to this later.
“One does not simply implement business logic”
This is somewhat similar to the early onset conservatism, yet there is a difference. Ever found yourself intimidated by a trivial task? Ask yourself this: would this task be intimidating if it was to be implemented in isolation, without the rest of the app surrounding it? If the answer is yes, look at your feet, because you might be caught in the trap. Implementing a feature in a well architected system shouldn’t be any more difficult than implementing it in isolation.
Sometimes a CMS trap can be recognized by the presence of phantom pains that stem from hidden implications of an emerging CMS. For example, in reality you would never need to delete your categories, but because you built them as admin-editable database-backed records, suddenly you are thinking about the non-existent scenario of having them deleted. Your architecture took the liberty of making you contemplate a scenario that isn’t real. You end up dealing with fake pains, the phantom pains.
All of the above symptoms have something in common. They are all a product of early assumptions that lead to a complex system. At this point it’s useful to answer two questions: “what’s a complex system?” and “how do you program without making assumptions?”.
Well, for the purposes of this essay let’s say that a complex system is a system of networked nodes which consists of more nodes and connections than you can generally track in your head. Obviously, to get a low-complexity system you need to reduce the number of nodes and connections. As for the latter question, that’s what brings me to the main point. In order to program without making early assumptions, you must avoid doing things at runtime.
Let me elaborate. Having been scarred by over-modelling, I found that there is a principle that should become fundamental in all decision making. Let’s call it: keep it static, stupid, which seems appropriate because it’s really nothing more than a slightly more architecturally-aware riff on keeping things simple.
Making things static is the architectural equivalent of avoiding premature optimization.
The beauty of this principle is that it’s applicable on every abstraction level, regardless of whether you are talking about views, database, or code. The idea itself is simple: if in doubt, do it statically. It’s easier to understand what this means by looking at some concrete examples on various levels of a typical Rails app.
Earlier in the post I mentioned pricing rules. This is a common problem where each product might abide by a different pricing algorithm. Price could depend on quantity, current user (think loyalty programs), order history, coupons, and various other things. To avoid the CMS trap I urge you not to allow constructing these kinds algorithms at runtime at an early stage. Write a pricing scheme class. Use strategy pattern. Make the pricing algorithm swappable at the code level. Define pricing rules via your programming language, this way the complexities of this logic can be mapped directly to code, and not warrant a whole layer of abstraction.
Programming languages already come with many wonderful tools, such as conditions and loops. Why reimplement them at a higher level of abstraction? These tools are more than enough to allow you to build complex pricing logic by writing code, directly. Once you have multiple pricing algorithms written as pluggable objects, feel free to let admin choose one, perhaps even “fill in the blanks” by plugging in factors and key values into your algorithm, but evolve this functionality gradually, as needed. Build out your admin UI with time, injecting more and more runtime flexibility into your strategy objects. Remember, you can always make static/hardcoded things dynamic, but not so much the other way. Everything that you make adjustable at runtime introduces complexity into every decision you make from that point on, and increases chances of bugs you cannot foresee, even in seemingly unrelated parts of your app.
Say you are listing things on a page for customer to see. These things may very well be products, photos, or files, whatever else it is that you are doing. Now, you have probably decided that there would be a title, a description, a picture, perhaps author or brand on each of those elements. You’ve split up your entities into these data fields and you decided to build database-backed models. This is where I’d suggest to stop and consider whether you have any good reason for why it can’t be a static page. A static templated view means that in order to change things, you have to edit the view and deploy, yes, but it also means that you don’t have controllers, models, migrations, forms, admin UI, or anything else. In fact, you might kind of still have an admin UI if you’re using Github. It’s not as real time as it could’ve been, but decent nonetheless. People can edit views on Github directly without much issue.
This becomes more of an issue if the things you list are categorized and otherwise laid out based on certain rules. In the dynamic approach, this would immediately force you to create a network of associated models just to render this sort of a page. Consider how little you know at this point about your future needs, and how constrained you will be having speculated your way towards that future. Consider also how quick and easy it would be to just sit down and hardcode this page. Just like the case with strategy pattern, you can always inject dynamic content into this page going forward, when the real needs arise. If you build out a dynamic system right away, you will likely end up constrained by it. Err on the side of static.
Getting back to the seed data issue, this example is fairly simple. You are creating categories. These categories are predefined. Instead of adding models, tables, and seed data, why not simply make a constant with an array? Code allows you to carry static data without involvement of a database. Use that, and wait until you really need the editing of categories at runtime. When that time comes, you could always extract data from the constant into the seed file, without any issues. Moreover, even that isn’t necessary. If you have some categories that never change, and some that should be manipulated in admin panel, you shouldn’t even seed the former ones. You could leave them in the constant, always read them from there, and this way avoid seed data altogether. It’s actually a little secret of mine. I don’t like seed data. It’s been years now, and our app works right out of the box on any new developer’s machine. If you pull our code into your dev machine, the app will just run. This is why I say: when in doubt, hardcode.
Say, at this point the app is working just fine, and you have your necessary database-backed models. You need to display a free-form text that may differ from one entity to another (e.g. different per product), yet it might contain certain values interpolated from elsewhere. As per the principle, you should not think about modelling this text via classes. First, ask whether you could simply get by with letting admin type the text as a string. But wait, you’d say. If this text has values plugged in from elsewhere, why should admin be typing them by hand? Would she have to look them up every time? Seems wrong. Well, relax a bit and consider canned snippets. That’s right, perhaps you can simply setup a free form text field while providing some pre-written text for admin, which has appropriate values already plugged in. When you think you need structured data for storing something highly flexible, consider instead using a plain string with canned snippets.
“For every complex problem there is an answer that is clear, simple, and wrong.”
— H. L. Mencken
While the above text is a good general principle, it cannot exactly apply to problems that are clearly asking for CMS-like solutions. When you are tasked with building a highly-flexible CMS, that’s what you do, naturally. When you are asked to build an app with something like Drupal, you are in a whole different realm, where the CMS trap is pretty much your perpetual state of being. However, even in those special cases questions will arise whether to make something more or less dynamic, and I encourage every developer to always lean towards static. You will be doing a service not only to your future self, but also to the next developer, who would much rather slice up a piece of static html and inject some dynamic content than attempt to understand a steaming pile of speculative architecture with many moving parts.
It’s also important to note that I’m not advocating entirely against architecting up front. It’s good to a healthy extent, yet there is a line we draw in the sand on a case by case basis. I encourage you to think carefully about where to draw that line every time you implement something.
Speaking of my story, it ended with a year-long stagnation and a very reluctant revamp of the entire app. In the end, the aforementioned “blueprints” have been downgraded to hardcoded classes, and over time they have become very declarative, thanks to a naturally-evolving internal DSL. Seeing these files today and imagining how I’d proceed implementing runtime admin UI for all the moving parts is nightmarish. Even though it ate away a year, I’m still glad that we bit the bullet and refactored. It was painful, but now this mistake is far behind us.
Unless you know exactly what you’re doing (which is unlikely), stay static. Try to put extra effort into determining which parts of your business can be left hardcoded. If in doubt, hardcode. While doing that, make sure you follow best practices: never put the same conditions in two places, never repeat constant data, use composition, dependency injection, inheritance, whatever you need to make sure you abide single responsibility principle, and maintain singular authority.
Most importantly, don’t get yourself tangled in too much speculation, let the story unfold naturally.