Plan of a Lifetime
A Pull Request Quest Part II
In the first part of this tale, I began making small contributions to open-source projects. I started laying out a plan to turn a gimmicky challenge into a semi-legitimate project, and I decided to do it in a pretty convoluted way. I also lied to you. It was a small lie, a fib really, and examining it raises interesting questions about software architecture. Most of what I claimed to be doing is still in the works. There will still be Python code running in containers. There will still be an abundance of database technologies. There will still be asynchronous events. I’m still going to intentionally over-engineer to the point of frustration for my own edification. What I won’t be doing though, not in several ways at once, is using a microservice architecture.
How do I work this?
Microservice architectures have some specific qualities: Small size, narrow focus and singular purpose, capable of being independently deployed, and loosely coupled. This is not a conclusive list, but it is the intersection of numerous popular and widely repeated definitions.
The topic is “micro” services, so it makes sense to start with size. I’m not sure about fitting inside James Lewis’ head, but the services I’m building feel small. They may not be code golf contenders, but each service's code base feels comfortably maintainable. They’re certainly not huge. I’m considering them small, or at least small enough. Each service also has a narrow focus. The complete solution involves two tasks: find repositories and inspect repositories. You can guess what the functions of the Finder and Inspector services are. These services are independently deployable, and they are loosely coupled. Services do not directly interface with one another. Each communicates results to a relational database server, and a message queue provides asynchronous integration. Looking again to those popular definitions of “microservices,” it certainly seems like that's what I’m designing. Popularity, though, has never had the strongest correlation with quality.
A more rigorous definition of microservices would include the alignment of services with a bounded context. In domain-driven design, a bounded context is a part of the domain model with consistent terms, definitions, and rules. Well-designed microservices align with the contexts of the associated problem domain. Different contexts use different languages, and microservices translate information crossing contextual boundaries. The services I am designing align with the bounded contexts of my problem domain, but it's a bit of a cheat. There is only one context in the domain model. The services "respect” the relevant contextual boundaries because there are none. Because information never passes between contexts, there is no need for translation. Is it possible the simplicity of the problem domain prevents the use of microservices? Could it be that these services are too small, too narrowly focused to be microservices?
How did I get here?
What do you call a set of small, narrowly focused, autonomous, and independently deployable services with shared context that function together to provide a solution? Anyone who learned SI units in school could guess the obvious answer is “nanoservices.”
There is no shortage of definitions available for “nanoservice,” and they range widely. There are many blog articles that describe nanoservices as being “like microservices but smaller.” Seems obvious enough. A more helpful but less encountered description goes on to state that each nanoservice has its own API endpoint and its own file. A strict 1:1:1 ratio of functions, endpoints, and code files. The product of a strictly literal interpretation of the microservices architecture. One service does exactly one thing, full-stop. Arnon Rotem-Gal-Oz labelled nanoservices as an antipattern “whose overhead outweighs its utility,” and that seems like the most helpful definition of all. Nanoservices strike me as a preoccupation with model definition. The inflexibility of nanoservices can provide guidance in every situation. It can reduce the burden of modelling the problem domain, but only at prohibitive cost.
The services I'm designing are not microservices. They certainly aren’t nanoservices or anything else smaller than a microservice. They’re just services, and services are great. What matters is a domain model that helps create a reliable system for delivering value to the user. Maximizing that value will require refinement of both the model and the services. Regardless of what they are called, the design of the services makes refactoring less burdensome. There's no need to be overly concerned with what the most accurate labels for the services are. Having said that, I am still curious. As I come to better understand microservices, the more doubt I have that their implementation was ever even possible.
Am I right? Am I wrong?
Conway’s Law, named after Melvin Conway, roughly states that when an organization designs a system the organization’s communication structure is represented in the system’s design. What then are the implications of Conway’s Law for a developer working on a team of one?
One way to look at it is that you can’t organize just one person. If there is no organization, then there is no organizational communication structure. Conway’s Law does not apply. It is true that there is no organizational structure facilitating communication between members on a team of one. Still, certain coding practices effectively constitute something analogous. A team of one communicates across time. A developer communicates with their future self by using variable names, comments, documentation, and commit messages. I know I’m not the only person to leave hastily written sticky notes on my screen when called away from working on some feature or fix. Communication from developer to developer exists in a team of one, but it is inherently simpler than the communication that occurs in formally structured organizations. That relative simplicity is represented in a system designed by an individual, just as Conway's law suggests. This lower complexity makes integral features of the microservice architecture inapplicable or greatly changes their implications.
For instance, take the matter of distribution. One advantage of a microservice architecture is the enablement of distributed development. Looking at my services, distributed development is compatible with their design. Unfortunately, significant value is lost in a team of one. Each service can be developed, deployed, and scaled independently. Continuous and independent refactoring of each service is enabled by their design. What the design can’t fully enable for a team of one is parallel development. Macro level parallel development is already enabled by using version control, but many actual applications of parallel development fail to apply. Parallelization can be used to isolate work on different features, but segregation of work between development teams is paradoxical. Customer-specific variants of software is a powerful offering, but it again loses meaning when developing for a customer base of one.
Microservices themselves are generally deployed as a distributed system. Unlike with distributed development, distributed deployment by a team of one presents no contradictions. There is, however, an issue of practicality. The application is a set of Docker images, so deploying it any number of distributed configuration is feasible. Right now though, the system is deployed locally to put it as close to both the data and the user as possible.
My God! What have I done?
So far, I have considered that my services are too narrow in scope to be microservices. I have considered that the problem domain outright precludes the use of microservices. But there is still one more way in which I am definitely not doing anything involving microservices, and it’s a big one. It’s the big one. One of the services is too big.
The Inspector service inspects documentation. That’s one thing, right? At one time, it was, but the Inspector is designed to allow for easy implementation of additional inspection tasks. I had already added a second type of inspection without a second thought. Now, in addition to checking for typos, the Inspector also looks for bad hypertext references. That’s definitely two things. And you know what happens when you start pulling at threads. Disentangling those inspection tasks reveals that writing the inspection reports is also its own task. The Inspector service obviously isn't a microservice. It's a macroservice, and it has the potential to become a monolithic pain as more inspection tasks are realized and implemented. Someone has some refactoring to do, and that someone is me.
I’ve been looking for something to center a live-coding stream around, and I think this could make for some sufficiently uncomfortable content. Either way, that’s for later. Sooner, I’ll be back with part three of whatever it is this series is becoming. Part three is a story featuring a metaphorical man who likes candy and no small amount of cognitive pain on my part. I'll also be making a serious effort to spend less time waxing philosophically on concepts I only tenuously grasp and more time talking about the specifics of the implementations. There will be code.