Dependency Injection (DI) is a widely adopted pattern that increases design flexibility in servers; however, it does not guarantee that a system will be easy to maintain. In this article, we present the “Pure Constructor Pattern” on top of DI that has the benefit of being more maintainable. The Pure Constructor Pattern enables us to iterate over changes quickly and test with less rewiring effort.
Many software developers follow Object Oriented (OO) patterns when writing libraries and services. For libraries, this is great; but for services, we are going to show a much simpler approach, some of which flies in the face of OO programming.
To drive the point home, we were once asked, “Is there a design document for this project?” Our answer was, “Draw out the design from the constructors.” The developer came back and told us how amazing that was.
Before diving into the details, let’s clearly define some terms that we’ll be seeing throughout our discussion:
NOTE: While your code and a framework’s code may live in the same jar (especially if you wrote the framework), it can be much easier to tell a framework from a library when they are separated out into their own jar files.
First, we’ll show part of a design. Then, we’ll show the implementation (our pattern) behind the design. Lastly, we’ll show a second implementation (an anti-pattern) that frequently exists in projects, but does not achieve the same benefits.
Below is a design of a routing service that finds and invokes routes. This router needs a translator for performing String to Object translation at different layers in the system.
Notice that the design above can simply be drawn out from the class constructors below. A developer that reads the code can reverse engineer it into a design in a matter of seconds. Understanding the deeper business logic is a separate issue.
First, we have the RouterService constructor which has a RouteFindAndInvoke and an ObjectTranslator just like the design above:
Moving down the graph, we then have the RouterFindAndInvoke constructor which has a RouteFinder and a RouteInvoker:
Next, we see that RouteFinder has no dependencies injected into its constructor, as we would expect from the diagram above:
And, finally, we note that RouteInvoker also refers to ObjectTranslator in its constructor:
As you can see from the above constructors, it’s easy to extrapolate the complete design of the system from its implementation by following the constructor chains. Most projects can easily have 90-95% of their code follow the Pure Constructor Pattern.
Please note that Request and Response classes are data-only objects with no business logic methods. Our pattern encourages passing around data-only classes from method to method.
Let’s look at some code that follows the same design, but implements it in a poor way. In RouterService, you will notice that ObjectTranslator is being passed into invoker.invoke on line 15. Because we are passing around business logic like this (via methods), it takes longer to figure out and understand the design, especially when the code is new. One must trace through where these business logic classes get passed around and how they’re invoked. We made this project very simple; but in a real project, the code is much more complex and many more things are being passed around, so understanding the design from this anti-pattern would be much harder.
Next, you will notice RouterFindAndInvoke has an invoke method below that then passes through the ObjectTranslator to routeInvoker.invoke on line 16:
Finally, in RouteInvoker, the translator is actually used:
Next, let’s take a look at four types of services we have used this pattern on with great success:
An example of the Request/Response type of service is basically a service that exposes some sort of API that you make requests to and receive responses from.
An example of the Streaming In/Out service is where the service cluster may process every Tweet being posted in real time (stream in), run a query against every Tweet, and only forward matching Tweets to the clients that uploaded that query (stream out).
A good example of the Loop service is one in which you want to send all users an email notice. In that example, you loop over all users in your system (perhaps once every month) and do something.
Then, as one more example, let’s combine request/response with streaming in. At Twitter, we stream all Tweets being posted in real time to a search cluster to store Tweets. Then, clients can send a query request to the search cluster API and get back a response with matching Tweets. This is a combination of streaming in plus request/response.
In all of these service types, you can apply this simple pattern creating an acyclic graph of stateless business classes. Then, any developer that joins your team can draw the design on their own (making ramp-up super fast). Again, this pattern is about team productivity rather than individual productivity.
This pattern is extremely useful for service development; and, if you have a ton of micro-services like Twitter does, it is an extremely useful pattern.
Many times, a developer comes across a problem and decides to create a framework. Sometimes this can be good, but most of the time, the approach relies heavily upon a pattern of inheritance. In general, we should prefer libraries over frameworks, because all of the same issues that pop up with inheritance also tend to pop up with frameworks. If you are not familiar with the concept of composition over inheritance, consider giving this a quick read. However, just as in composition over inheritance, this does not mean you should always use libraries and never use frameworks. We will not go into detail, but there are many examples on the web of converting inheritance into composition. Frameworks are very similar to inheritance and can be flipped to libraries by following these patterns.
Ideally, keep your threads closer to the edge of the system. This does a few things:
Let’s start with our simple request/response example. In an ideal situation, we use a server framework that puts the thread pool between the request I/O and invoking the API. The threads handle the I/O request and then invoke the API containing our service logic. This way there is no business logic outside the thread pool, and our tests can call the API and thus our service in the same way a client would, but in a simple single-thread model.
Next is our streaming in/out example. In this case, rather than have threads deep in the system “pull” data in, move the threads out to the edge to read from the network and “push” data into the system. This makes it easy for a test to simply push data into the system using the same API that the thread pool calls.
In cases where you have a thread pool in the middle of the system, we highly advise using the Executor class in Java which has one method, run(Runnable r), so that you can swap in a DirectExecutor during tests, eliminating the thread pool so that all tasks run on the same thread.
Pushing threads to the edge of your system makes it much easier to step through and debug.
A successful product requires team success. This demands a thought process that is conducive to putting long-term maintainability toward the top of a team’s priorities. Doing so allows a team to effectively address turnover, easily extend its systems, and manage and maintain code in an iterative, agile manner. By following this componentized simple service pattern, you enable your team to maintain an easily traversable code base that’s more straightforward to test and contains less “spaghetti” in both code and logic. While this approach requires additional thought and organization, this upfront effort pays compounding dividends to you as your team and problem space grow.