Insights

One pattern to rule them all

By Matt Gross and Dean Hiller
Wednesday, 25 September 2019

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.

Advantages

  • Your architecture is easier to visualize, as dependencies are clear.
  • Your service is easily modified or changed with minimal collateral work required.
  • Dependencies are directed and acyclic (DAG), which guards against needlessly overcomplicating or hardening the system design.
  • Business logic is better consolidated in each respective class.
  • Design is more straightforward, which often leads to better performance.
  • It’s easier to feature test!

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.

This Tweet is unavailable
This Tweet is unavailable.

Definitions

Before diving into the details, let’s clearly define some terms that we’ll be seeing throughout our discussion:

  • Pattern: A documented approach which outlines a high-level solution to a common design problem.
  • Design: Components and their relationships which, together, make up a service.
  • Implementation: The internals of a design, usually specific code.
  • Framework: If you have code in a jar that you import and that code calls methods in your code, then that jar is a framework.
  • Library: If you have code in a jar that you import and your code calls methods in that code, then that jar is a library.
  • Thread pool: A group of idle threads which wait, ready to be given work.
  • Edge of the system: The point of contact for a client to use your system (e.g., an API endpoint that you would serve).

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.

This Tweet is unavailable
This Tweet is unavailable.

The design

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.

This Tweet is unavailable
This Tweet is unavailable.

Our pattern

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:

This Tweet is unavailable
This Tweet is unavailable.

Moving down the graph, we then have the RouterFindAndInvoke constructor which has a RouteFinder and a RouteInvoker:

This Tweet is unavailable
This Tweet is unavailable.

Next, we see that RouteFinder has no dependencies injected into its constructor, as we would expect from the diagram above:

This Tweet is unavailable
This Tweet is unavailable.

And, finally, we note that RouteInvoker also refers to ObjectTranslator in its constructor:

This Tweet is unavailable
This Tweet is unavailable.

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.

This Tweet is unavailable
This Tweet is unavailable.

Anti-pattern

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.

This Tweet is unavailable
This Tweet is unavailable.

Next, you will notice RouterFindAndInvoke has an invoke method below that then passes through the ObjectTranslator to routeInvoker.invoke on line 16:

This Tweet is unavailable
This Tweet is unavailable.

Finally, in RouteInvoker, the translator is actually used:

This Tweet is unavailable
This Tweet is unavailable.

Four types of services

Next, let’s take a look at four types of services we have used this pattern on with great success:

  • Request/Response
  • Streaming In/Out
  • Loop
  • A combination of the above

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 Tweet is unavailable
This Tweet is unavailable.

Steps to implement

  • Make sure data objects are just data with very little, if any, business logic methods
  • Prefer libraries over framework creation
  • Prefer composition over inheritance
  • Make sure all business objects are injected into constructors
  • Push all threads to the edges of the system (best effort)

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.

Prefer libraries over frameworks

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.

Threading

Ideally, keep your threads closer to the edge of the system. This does a few things:

  • It is much easier to create a feature test that covers the whole system.
  • It keeps the system easier to debug.
  • It is easier for a new developer to follow the flow of the thread.

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.

This Tweet is unavailable
This Tweet is unavailable.

Conclusion

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.

This Tweet is unavailable
This Tweet is unavailable.
@mattkgross

Matt Gross

‎@mattkgross‎

Software Engineer

@drballstothewal

Dean Hiller

‎@drballstothewal‎

Senior Software Engineer

Only on Twitter