I’ve been slowly building a new version of this website, to replace wordpress (and its attendant problems and constant updates) with something a little lighter that I control more tightly (I’m not just rolling my own blog engine, but a platform for several other things I have planned). As I’ve done so, I’ve been rethinking a lot of my approaches to software development for personal projects (corporate projects are a different matter and under different constraints). Given the ever-accelerating rate at which frameworks change, especially client-side javascript frameworks, I’ve had to more carefully assess the risk of major breaking changes and attempt to mitigate those as I switch things over. I’m currently building a fairly lightweight blog editor that allows me to create new blogposts using markdown and publish the rendered HTML in the actual blog itself. None of it is rocket science, but there are a lot of little things to think through. Perhaps one of the biggest things I’ve noticed is how different frameworks deal with extensibility versus ease of initial setup. I’m going to use javascript frameworks as an example of the sort of design decisions I’m dealing with, because it often seems to be the worst offender for me, although it’s likely that much of the difficulty I have on occasion will go away once I’m a little closer to a decade of solid javascript usage (I really only started using javascript heavily in the early days of jquery, although I used it under duress all the way back). While I was originally a fan of larger, more comprehensive systems, such as AngularJS because they handled much of the internals of the front end of the application, I’ve since started to move away from such systems precisely because they attempt to handle the internals of so much of the front end all in one place.
I’m becoming a big fan of building applications by tying together numerous smaller components, as it seems to “reduce the surface area on which bad luck strikes”, as it were. There are numerous reasons I prefer composing applications of tight, smaller components rather than using larger frameworks.
- Unit test coverage. Smaller components, in my experience, tend to be much more heavily tested in the sort of environments in which they are used, precisely because those environments are far more limited in number. I suspect that it is substantially easier for (for instance) the KnockoutJS team to support old versions of Internet Explorer than it is for a team managing something as large as AngularJS to do the same. It’s not that it’s actually easy for them to do so, more that it’s actually possible for them to do so. It’s also easier to support an old branch for an older browser version as well, which makes it more likely that you’ll have an upgrade path.
- Assumption of extension. Smaller components also tend to be structured in such a way as to allow them to work with as many other components as possible. This assumption forces the component developer to pay careful attention to the points at which their component will be interacting with other components, rather than pushing them towards first building a comprehensive system that covers all possible cases (that the developer can think of, which will absolutely ruin your day when, not if, you run into an unsupported edge case while building something). This assumption that the component will be extended and or loosely integrated with other components also makes it far easier for a community to develop around doing just that. This potentially means that other libraries will be available to partially or totally bridge the gap between components.
- Smaller update granularity. Of late, I find myself running into projects with rather substantial updates to large critical components (hello ASP.NET and webforms, AngularJS, and bootstrap), with a pretty substantial upgrade path required. The upgrades were all needed because of major underlying concerns and the upgrades were all improvements, however, it did (or soon will) take a fair bit of work to move forward to a new version of these products. I find that tends to result in the project staying on the old codebase for far longer, usually until the old version is about stop being supported entirely. For instance, the recent changes to AngularJS generated a bit a firestorm because it is such a large piece of so many people’s codebases (that a reasonable upgrade path is being built in that case is little comfort, since that doesn’t always happen). With smaller components, the surface area of changes tends to be smaller as well, so it’s easier to update and quickly test to make sure everything worked properly.
- Faster iteration. With larger projects, deployment of a new generally available version requires a lot more work. From testing, to writing up release notes, to upgrading documentation, smaller components require fewer people to be involved in order to release. I find that this tends to improve the likelihood of getting a bug fixed within a reasonable period of time, as the smaller amount of code involved reduces the probability of regressions. It also makes it easier for me to update a single, small part of an application at a time, which allows to apply smaller updates as they occur, rather than dealing with a bunch at once, which may well be entangled with each other.
- Ease of switching out pieces. It requires a lot less work to remove and replace a troublesome or otherwise non-optimal component if it is small than it does to replace a much larger component that is entangled into your system. As developers we all know, that the software 3 or 4 product versions after the initial release may well be radically different than what was initially planned. This is especially true in small startups, where a quick pivot into an adjacent market vertical may be required to actually keep a revenue stream for the product. As a rule, you don’t want to be using something that gets in your way when this happens. Being able to slowly replace one or several smaller components is much less painful and risky from a project management and release cycle perspective than is the replacement of a single large component that is tied into huge swaths of a system that is already in production.
That said, there are a few issues with the approach that have cropped up. These are things you may want to consider if you are intend to use a more modular design in your applications.
- Initial setup can be tricky. You’ll have to figure out how to get all your various components to talk to each other before you can really get going, possibly even doing extra design work in order to keep them from interfering with each other during operation. Most of that cost is loaded up front, while the cost of using a big integrated framework is often on the back end.
- You aren’t going to find developers who know your stack. Developers are already in short supply, but it’s within reason to be able to find one that can plug a couple of large frameworks together, who is already trained to do so by someone else. But if you have several smaller components strung together, it’s unlikely you’ll find a developer who has worked with that precise development stack before. They aren’t going to be able to just jump in there on day one. That said, this may not be much of an issue with a carefully designed system, but it is something to consider if you are hiring outside help.
- You will need automated tests. As you continue to develop the application and apply updates to individual components, you’ll need a good way to quickly test and at least make sure that updates haven’t broken some other part of the system in a subtle way. And you’ll want at least this part of your tests automated, because smaller components tend to have a faster update cadence. You don’t want to be manually running tests every time one of them upgrades. While I recognize that you should have a full suite of automated tests to check your entire application, I also know full well that very few people do. This, however, is one of the areas where you really don’t want to let testing slide.
- You will have to isolate components. You’ll need to be careful to make sure that bad interactions are contained. It’s very easy, especially in environments like browser javascript, to end up in a situation where one component overwrites a variable that another component requires, or formats a date in a strange way. You may wish to make your own shims to sit between components and control communication in some critical situations, simply for the damage control value it provides.
- You will write more code. This approach doesn’t save on code. One of the larger intents of bigger frameworks is the reduction in the amount of code you need to write. That’s usually not the way things worth with smaller components, as you have to stitch them together in addition to doing whatever you are trying to do. On the other hand, you aren’t as likely to have to deep dive into the innards of a small project to get a one-off requirement working either. In my experience, this evens out, with the model of using multiple smaller components instead of a single larger one winning out because of the smaller risk of having to interact with the system in an unexpected way.
At the end of the day, I’ve found that putting together a lot of smaller components to build an application is preferable to using one or two big frameworks, as the latter have a lot of maintenance costs whose timing and extent are much harder to control. I also think this paradigm produces higher quality components, as small teams or individuals can work on them more easily than a similar team can handle a larger project. Finally, it allows me to (relatively) quickly shift to updated code while not destroying project timelines due to an unforeseen, large update.