3. Structure

The previous chapter discussed the universal design principle of volatility-based decomposition. This principle governs the design of all practical systems—from houses, to laptops, to jumbo planes, to your own body. To survive and thrive, they all encapsulate the volatility of their constituent components. Software architects only have to design software systems. Fortunately, these systems share common areas of volatility. Over the years I have found these common areas of volatility within hundreds of systems. Furthermore, there are typical interactions, constraints, and run-time relationships between these common areas of volatility. If you recognize these, you can produce correct system architecture quickly, efficiently, and effectively.

Given this observation, The Method provides a template for the areas of volatility, guidelines for the interaction, and recommends operational patterns. By doing so, The Method goes beyond mere decomposition. Being able to furnish such general guidelines and structure across most software systems may sound far-fetched. You may wonder how these kinds of broad strokes could possibly apply across the diversity of software systems. The reason is that good architectures allow use in different contexts. For example, a mouse and an elephant are vastly different, yet they use identical architecture. The detailed designs of the mouse and the elephant, however, are very different. Similarly, The Method can provide you with the system architecture, but not its detailed design.

This chapter is all about The Method’s way of structuring a system, the advantages this brings, and its implications on the architecture. You will see classification of services based on their semantics and the associated guidelines, as well as how to layer your design. In addition, having clear, consistent nomenclature for components in your architecture and their relationship brings two other advantages. First, it provides a good starting point. You will still have to sweat over it, but at least you start at a reasonable point. Second, it improves communication because you can now convey your design intent to other architects or developers. Even communicating with yourself in this way is very valuable, as it helps to clarify your own thoughts.

Use Cases and Requirements

Before diving into architecture, consider requirements. Most projects, if they even bother to capture the requirements, use functional requirements. Functional requirements simply state the required functionality, such as “The system should do A.” This is actually a poor way of specifying requirements, because it leaves the system’s implementation of the A functionality open for interpretation. In fact, functional requirements allow for multiple opportunities for misinterpretations to arise between the customers and marketing, between marketing and engineering, and even between developers. This kind of ambiguity tends to persist until you have already spent considerable effort on developing and deploying the system, at which point rectifying it is the most expensive.

Requirements should capture the required behavior rather than the required functionality. You should specify how the system is required to operate as opposed to what it should do, which is arguably the essence of requirements gathering. As with most other things, this does take additional work and effort (something that people in general try to avoid), so getting requirements into this form will be an uphill struggle.

Required Behaviors

A use case is an expression of required behavior—that is, how the system is required to go about accomplishing some work and adding value to the business. As such, a use case is a particular sequence of activities in the system. Use cases tend to be verbose and descriptive. They can describe end-user interactions with the system, or the system’s interactions with other systems, or back-end processing. This ability is important because in any well-designed system, even one of modest size and complexity, the users interact with or observe just a small part of the system, which represents the tip of the iceberg. The bulk of the system remains below the waterline, and you should produce use cases for it as well.

You can capture use cases either textually or graphically. Textual use cases are easy to produce, which is a distinct advantage. Unfortunately, using text for use cases is an inferior way of describing use cases because the use cases may be too complex to capture with high fidelity in text. The real problem with textual use cases is that hardly anyone bothers to read even simple text, and for a good reason. Reading is an artificial activity for the human brain, because the brain is not wired to easily absorb and process complex ideas via text. Mankind has been reading for 5000 years—not long enough for the brain to catch up, evolutionarily speaking (thank you for making the effort with this book, though).

The best way of capturing a use case is graphically, with a diagram (Figure 3-1). Humans perform image processing astonishingly quickly, because almost half the human brain is a massive video processing unit. Diagrams allow you to take advantage of this processor to communicate ideas to your audience.

Figure 3-1 A use case diagram

Graphical use cases, however, can be very labor-intensive to produce, especially in large numbers. Many use cases may be simple enough to understand without a diagram. For example, the use case diagram in Figure 3-1 can be represented in text equally well. My rule of thumb: The presence of a nested “if” tells you that you should to draw the use case. No reader can parse a sentence containing a nested “if.” Instead, readers will likely continually reread the use case or, more likely, pick up a pen and paper and try to visualize the use case themselves. By doing so, readers are interpreting the behavior—which also raises the possibility of misinterpretation. When readers are scribbling on the side of your textual use case, you know you should have provided the visualization in the first place. Diagrams also allow readers to easily follow a larger number of nested “if”s in a complex use case.

Activity Diagrams

The Method prefers activity diagrams1 for graphical representation of use cases, primarily because activity diagrams can capture time-critical aspects of behavior, something that flowcharts and other diagrams are incapable of doing. You cannot represent parallel execution, blocking, or waiting for some event to take place in a flowchart. Activity diagrams, by contrast, incorporate a notion of concurrency. For example, in Figure 3-2, you intuitively see the handling of parallel execution as a response to the event without even seeing a notation guide for the diagram. Note also how easy it is to follow the nested condition.

Figure 3-2 An activity diagram

1. https://en.wikipedia.org/wiki/Activity_diagram

Caution

Do not confuse activity diagrams with use case diagrams. Use case diagramsa are user-centric and should have been called user case diagrams. Use case diagrams also do not include a notion of time or sequence.

a. https://en.wikipedia.org/wiki/Use_case_diagram

Layered Approach

Software systems are typically designed in layers, and The Method relies heavily on layers. Layers allow you to layer encapsulation. Each layer encapsulates its own volatilities from the layers above and the volatilities in the layers below. Services inside the layers encapsulate volatility from each other, as shown in Figure 3-3.

Figure 3-3 Services and layers

Even simple systems should be designed in layers to gain the benefit of encapsulation. In theory, the more layers, the better the encapsulation. Practical systems will have only a handful of layers, terminating with a layer of actual physical resources such as a data storage or a message queue.

Using Services

The preferred way of crossing layers is by calling services. While you certainly can benefit from the structure of The Method and volatility-based decomposition even with regular classes, relying on services provides distinct advantages. Which technology and platform you use to implement your services is a secondary concern. When you do use services (as long as the technology you chose allows), you immediately gain the following benefits:

  • Scalability. Services can be instantiated in a variety of ways, including on a per-call basis. This allows for a very large number of clients without placing a proportional load on the back-end resources, as you need only as many service instances as there are calls in progress.

  • Security. All service-oriented platforms treat security as a first-class aspect. Thus, they authenticate and authorize all calls—not just those from the client application to the services, but also those between services. You can even use some identity propagation mechanism to support a chain-of-trust pattern.

  • Throughput and availability. Services can accept calls over queues, allowing you to handle a very large volume of messages by simply queuing up the excess load. Queued calls also enable availability, because you can have multiple service instances process the same incoming queue.

  • Responsiveness. Services can throttle the calls into a buffer to avoid maxing out the system.

  • Reliability. Clients and services can use some reliable messaging protocol to guarantee delivery, handle network connectivity issues, and even order the calls.

  • Consistency. The services can all participate in the same unit of work, either in a transaction (when supported by the infrastructure) or in a coordinated business transaction that is eventually consistent. Any error along the call chain causes the entire interaction to abort, without coupling the services along the nature of the error and the recovery logic.

  • Synchronization. The calls to the service can be automatically synchronized even if the clients use multiple concurrent threads.

Typical Layers

The Method calls for four layers in the system architecture. These layers conform to some classic software engineering practices. However, using volatility to drive the decomposition inside these layers may be new to you. Figure 3-4 depicts the typical layers in The Method.

Figure 3-4 Typical layers in The Method

The Client Layer

The top layer in architecture is the client layer, also known as the presentation layer. I find the term “presentation” to be somewhat misleading. “Presentation” implies some information is being presented to human users, as if that is all that is expected from the top layer. The elements in the client layer may very well be end-user applications, but they can also be other systems interacting with your system. This is an important distinction: By calling this the client layer, you equalize all possible clients, treating them in the same way. All Clients (whether end-user applications or other systems) use the same entry points to the system (an important aspect of any good design) and are subject to the same access security, data types, and other interfacing requirements. This, in turn, promotes reuse and extensibility and allows for easier maintenance, as a fix at one entry point affects all Clients the same way.

Having the Clients consume services caters to better separation of presentation from business logic. Most service-oriented technologies are very strict about the types of data they allow over the endpoints. This limits the ability to couple the Clients to the services, treats all Clients uniformly, and makes adding different types of Clients, at least in theory, easier to accomplish.

The client layer also encapsulates the potential volatility in Clients. Your system now and in the future across the axes of volatility may have different Clients such as desktop applications, web portals, mobile apps, holograms and augmented reality, APIs, administration applications, and so on. The various Client applications will use different technologies, be deployed differently, have their own versions and life cycles, and may be developed by different teams. Indeed, the client layer is often the most volatile part of a typical software system. However, all of that volatility is encapsulated in the various blocks of the client layer, and changes in one component do not affect another Client component.

The Business Logic Layer

The business logic layer encapsulates the volatility in the system’s business logic. This layer implements the system’s required behavior, which, as mentioned previously, is best expressed in use cases. If the use cases were static, there would be no need for a business logic layer. Use cases, however, are volatile, across both customers and time. Since a use case contains a sequence of activities in the system, a particular use case can change in only two ways: Either the sequence itself changes or the activities within the use case change. For example, consider the use case in Figure 3-1 versus the use cases in Figure 3-5.

Figure 3-5 Sequence volatility

All four use cases in Figures 3-1 and 3-5 use the same activities A, B, and C, but each sequence is unique. The key observation here is that the sequence or the orchestration of the workflow can change independently from the activities.

Now consider the two activity diagrams in Figure 3-6. Both call for exactly the same sequence, but they use different activities. The activities can change independently from the sequence.

Figure 3-6 Activity volatility

Both the sequence and the activities are volatile, and in The Method these volatilities are encapsulated in specific components called Managers and Engines. Manager components encapsulate the volatility in the sequence, whereas Engine components encapsulate the volatility in the activity. In Chapter 2, in the stock trading decomposition example, the Trade Workflow component (see Figure 2-13) is a Manager, while the Feed Transformation component is an Engine.

Since use cases are often related, Managers tend to encapsulate a family of logically related use cases, such as those in a particular subsystem. For example, with the stock trading system of Chapter 2, Analysis Workflow is a separate Manager from Trade Workflow, and each Manager has its own related set of use cases to execute. Engines have more restricted scope and encapsulate business rules and activities.

Since you can have great volatility in the sequence without any volatility in the activities of the sequence (see Figure 3-5), Managers may use zero or more Engines. Engines may be shared between Managers because you could perform an activity in one use case on behalf of one Manager and then perform the same activity for another Manager in a separate use case. You should design Engines with reuse in mind. However, if two Managers use two different Engines to perform the same activity, you either have functional decomposition on your hands or you have missed some activity volatility. You will see more on Managers and Engines later in this chapter.

The Resourceaccess Layer

The aptly named resource access layer encapsulates the volatility in accessing a resource, and the components in this layer are called ResourceAccess. For example, if the resource is a database, literally dozens of methods are available for accessing a database, and no single method is superior to all other methods in every respect. Over time, you may want to change the way you access the database, so that change or the volatility involved should be encapsulated. Note that you should not simply encapsulate the volatility in accessing the resource; that is, you must also encapsulate the volatility in the resource itself, such as a local database versus a cloud-based database, or in-memory storage versus durable storage. Resource changes invariably change ResourceAccess as well.

While the motivation behind the resource access layer is readily evident and many systems incorporate some form of an access layer, most such layers end up exposing the underlying volatility by creating a ResourceAccess contract that resembles I/O operations or that is CRUD-like. For example, if your ResourceAccess service contract contains operations such as Select(), Insert(), and Delete(), the underlying resource is most likely a database. If you later change the database to a distributed cloud-based hash table, that database-access-like contract will become useless, and a new contract is required. Changing the contract affects every Engine and Manager that has used the ResourceAccess component. Similarly, you must avoid operations such as Open(), Close(), Seek(), Read(), and Write() that betray the underlying resource as being a file. A well-designed ResourceAccess component exposes in its contract the atomic business verbs around a resource.

Use Atomic Business Verbs

The Manager services in the system execute some sequence of business activities. These activities, in turn, often comprise an even more granular set of activities. However, at some point you will have such low-level activities that they cannot be expressed by any other activity in the system. The Method refers to these indivisible activities as atomic business verbs. For example, in a bank, a classic use case would be to transfer money between two accounts. The transfer is done by crediting one account and debiting another. In a bank, credit and debit are atomic operations from the business’s perspective. Note that an atomic business verb may require several steps from the system perspective to implement. The atomicity is geared toward the business, not the system.

Atomic business verbs are practically immutable because they relate strongly to the nature of the business, which, as discussed in Chapter 2, hardly ever changes. For example, since the time of the Medici, banks have performed credit and debit operations. Internally, the ResourceAccess service should convert these verbs from its contract into CRUDs or I/O against the resources. By exposing only the stable atomic business verbs, when the ResourceAccess service changes, only the internals of the access component change, rather than the whole system atop it.

ResourceAccess Reuse

ResourceAccess services can be shared between Managers and Engines. You should explicitly design ResourceAccess components with this reuse in mind. If two Managers or two Engines cannot use the same ResourceAccess service when accessing the same resource or have some need for specific access, perhaps you did not encapsulate some access volatility or did not isolate the atomic business verbs correctly.

The Resource Layer

The resource layer contains the actual physical Resources on which the system relies, such as a database, file system, a cache, or a message queue. In The Method, the Resource can be internal to the system or outside the system. Often, the Resource is a whole system in its own right, but to your system it appears as just a Resource.

Utilities Bar

The utilities vertical bar on the right side of Figure 3-4 contains Utility services. These services are some form of common infrastructure that nearly all systems require to operate. Utilities may include Security, Logging, Diagnostics, Instrumentation, Pub/Sub, Message Bus, Hosting, and more. You will see later in this chapter that Utilities require different rules compared with the other components.

Classification Guidelines

As is true for every good idea, The Method can be abused. Without practice and critical thinking, it is possible to use The Method taxonomy in name only and still produce a functional decomposition. You can mitigate this risk to a great extent by adhering to the simple guidelines provided in this section.

Another use for leveraging guidelines is initiating design. At the beginning of nearly every design effort, most people are stumped, unsure where to even start. It is very helpful to be armed with some key observations that can both initiate and validate a budding design effort.

What’s in a Name

Service names as well as diagrams are important in communicating your design to others. Descriptive names are so important within the business and resource access layers that The Method recommends the following conventions for naming them:

  • Names of services must be two-part compound words written in Pascal case.

  • The suffix of the name is always the service’s type—for example, Manager, Engine, or Access (for ResourceAccess).

  • The prefix varies with the type of service.

    –  For Managers, the prefix should be a noun associated with the encapsulated volatility in the use cases.

    –  For Engines, the prefix should be a noun describing the encapsulated activity.

    –  For ResourceAccess, the prefix should be a noun associated with the Resource, such as data that the service provides to the consuming use cases.

  • Gerunds (a gerund is a noun created by tacking “ing” onto a verb) should be used as a prefix only in with Engines. The use of gerunds elsewhere in the business or access layers usually signals functional decomposition.

  • Atomic business verbs should not be used in a prefix for a service name. These verbs should be confined to operation names in contracts interfacing with the resource access layer.

As examples, in a bank design, AccountManager and AccountAccess are acceptable service names. However, the names BillingManager and BillingAccess smell of functional decomposition because the gerund prefixes convey a concept of “doing” rather than of an orchestration or access volatility. CalculatingEngine is a good candidate name because Engines “do” things such as aggregate, adapt, strategize, validate, rate, calculate, transform, generate, regulate, translate, locate, and search. The name AccountEngine, by contrast, is devoid of any indicator of the activity volatility and again carries a strong smell of functional or domain decomposition.

The Four Questions

The layers of services and resources in the architecture loosely correspond to the four English questions of “who,” “what,” “how,” and “where.” “Who” interacts with the system is in the Clients, “what” is required of the system is in Managers, “how” the system performs business activities is in Engines, “how” the system accesses Resources is in ResourceAccess, and “where” the system state is in Resources (see Figure 3-7).

Figure 3-7 Questions and layers

The four questions loosely correspond to the layers because volatility trumps everything. For example, if there is little or no volatility in the “how,” the Managers can perform both “what” and “how.”

Asking and answering the four questions is useful at both ends of the design effort, for initiation and for validation. If all you have is a clean slate and no clear idea where to start, you can initiate the design effort by answering the four questions. Make a list of all the “who” and put them in one bin as candidates for Clients. Make a list of all the “what” and put them in another bin as candidates for Managers, and so on. The result will not be perfect—for example, all “what” components will not necessarily coalesce into individual Managers—but it is a start.

Once you complete your design, take a step back and examine the design. Are all your Clients “who,” with no trace of “what” in them? Are all the Managers “what,” without a smidgen of “who” and “where” in them? Again, the mapping of questions to layers will not be perfect. In some cases, you could have crossover between questions. However, if you are convinced the encapsulation of the volatility is justified, there is no reason to doubt that choice further. If you are unconvinced, the questions could indicate a red flag and a decomposition to investigate.

The four questions tie in nicely with the previous guideline on naming the services. If the Manager prefixes describe the encapsulated volatilities, it is more natural to talk about them in terms of “what” as opposed to the verb-like “how.” If the Engine prefixes are gerunds describing the encapsulated activities, it is more natural to talk about them in terms of “how” as opposed to “what” or “where.” For similar reasons, ResourceAccess encapsulates “how” to access the Resources that lie behind it.

Managers-to-Engines Ratio

Most designs end up with fewer Engines than you might initially imagine. First, for an Engine to exist, there must be some fundamental operational volatility that you should encapsulate—that is, an unknown number of ways of doing something. Such volatilities are uncommon. If your design contains a large number of Engines, you may have inadvertently done a functional decomposition.

In our work at IDesign, we have observed across numerous systems that Managers and Engines tend to maintain a golden ratio. If your system has only one Manager (not a god service), you may have no Engines, or at most one Engine. Think about it: If the system is so simple that one decent Manager suffices, how likely is it to have high volatility in the activities but not that many types of use cases?

Generally, if your system has two Managers, you will likely need one Engine. If your system has three Managers, two Engines is likely the best number. If your system has five Managers, you may need as many as three Engines. If your system has eight Managers, then you have already failed to produce a good design: The large number of Managers strongly indicates you have done a functional or domain decomposition. Most systems will never have that many Managers because they will not have many truly independent families of use cases with their own volatility. In addition, a Manager can support more than one family of use cases, often expressed as different service contracts, or facets of the service. This can further reduce the number of Managers in a system.

Key Observations

Armed with the recommendations of The Method, you can make some sweeping observations about the qualities you expect to see in a well-designed system. Deviating from these observations may indicate a lingering functional decomposition or at least an unripe decomposition in which you have encapsulated few of the glaring volatilities but have missed others.

Volatility Decreases Top-Down

In a well-designed system, volatility should decrease top-down across the layers. Clients are very volatile. Some customers may want the Clients this way, other customers would want the Clients that way, and others may want the same thing but on a different device. This naturally high level of volatility has nothing to do with the required behavior of the underlying system. Managers do change, but not as much as their Clients. Managers change when the use cases—the required behavior of the system—change. Engines are less volatile than Managers. For an Engine to change, your business must change the way it is performing some activity, which is more uncommon than changing the sequencing of activities. ResourceAccess services are even less volatile than Engines. How often do you change the way you access a Resource or, for that matter, change the Resource? You can change activities and their sequence without ever changing the mapping of the atomic business verbs to Resources. Resources are the least volatile components, changing at a glacial pace compared with the rest of the system.

A design in which the volatility decreases down the layers is extremely valuable. The components in the lower layers have more items that depend on them. If the components you depend upon the most are also the most volatile, your system will implode.

Reuse Increases Top-Down

Reuse, unlike volatility, should increase going down the layers. Clients are hardly ever reusable. A Client application is typically developed for a particular type of platform and market and cannot be reused. For example, the code in a web portal cannot easily be reused in a desktop application, and the desktop application cannot be reused in a mobile device. Managers are reusable because you can use the same Manager and use cases from multiple Clients. Engines are even more reusable than Managers because the same Engine could be called by multiple Managers, in different use cases, to perform the same activity. ResourceAccess components are very reusable because they can be called by Engines and Managers. The Resources are the most reusable element in any well-designed systems. The ability to reuse existing Resources in a new design is often a key factor in business approval of a new system’s implementation.

Almost-Expendable Managers

Managers can fall into one of three categories: expensive, expendable, and almost expendable. You can distinguish the category to which a Manager belongs by the way you respond when you are asked to change it. If your response is to fight the change, to fear its cost, to argue against the change, and so forth, then the Manager was clearly expensive and not expendable. An expensive Manager indicates that the Manager is too big, likely due to functional decomposition. If your response to the change request is just to shrug it off, thinking little of it, the Manager is pass-through and expendable. Expendable Managers are always a design flaw and a distortion of the architecture. They often exist only to satisfy the design guidelines without any real need for encapsulating use case volatility.

If, however, your response to the proposed Manager change is contemplative, causing you to think through the specific ways of adapting the Manager to the change in the use case (perhaps even quickly estimating the amount of work required), the Manager is almost expendable. If the Manager merely orchestrates the Engines and the ResourceAccess, encapsulating the sequence volatility, you have a great Manager service, albeit an almost expendable one. A well-designed Manager service should be almost expendable.

Subsystems and Services

The Managers, Engines, and ResourceAccess are all services on their own right. A cohesive interaction between the Manager, Engines, and ResourceAccess may constitute a single logical service to external consumers. You can view such a set of interacting services as a logical subsystem. You group these together as a vertical slice of your system (Figure 3-8), where each vertical slice implements a corresponding set of use cases.

Figure 3-8 Subsystems as vertical slices

Avoid over-partitioning your system into subsystems. Most systems should have only a handful of subsystems. Likewise, you should limit the number of Managers per subsystem to three. This also allows you to somewhat increase the total number of Managers in the system across all subsystems.

Incremental Construction

If the system is relatively simple and small, the business value of the system—that is, the execution of the use cases—will likely require all components of the architecture. For such systems, there is no sense in releasing, say, just the Engines or the ResourceAccess components.

With a large system, it could be that certain subsystems (such as the vertical slices of Figure 3-8) can stand alone and provide direct business value. Such systems will be more expensive to build and take longer to complete. In such cases it makes sense to develop and deliver the system in stages, one slice at a time, as opposed to providing a single release at the end of the project. Moreover, the customer will be able to provide early feedback to the developers on the incremental releases as opposed to only the complete system at the end.

With both small and large systems, the right approach to construction is another universal principle:

Design iteratively, build incrementally.

This principle is true regardless of domain and industry. For example, suppose you wish to build your house on a plot of land you have purchased. Even the best architect will not be able to produce the design for your house in a single session. There will be some back-and-forth as you define the problem and discuss constraints such as funds, occupants, style, time, and risk. You will start with some rough cuts to the blueprints, refine them, evaluate the implications, and examine alternatives. After several of these iterations, the design will converge. When it is time to build the house, will you do that iteratively, too? Will you start with a two-person tent, grow it out to a four-person tent, then to a small shed, then to a small house, and finally to a bigger house? It would be insane to even contemplate such an approach. Instead, you are likely to dig and cast the foundation, then erect the walls to the first floor, then connect utilities to the structure, then add the second floor, and finally add the roof. In short, you build a simple house incrementally. There is no value for the prospective homeowner in having just the foundations or the roof. That is, the house—like an incrementally built simple software system—has no real value until complete. However, if the building has multiple floors (or multiple wings), it may be possible to build it incrementally and deliver intermediate value. Your design may allow you to complete one floor at a time (or one wing at a time), similar to the “one slice at a time” approach to a large software system.

Another example is assembling cars. While the car company may have had a team of designers designing a car across multiple iterations, when it is time to build the car, the manufacturing process does not start with a skateboard, grow that to a scooter, then a bicycle, then a motorcycle, and finally a car. Instead, a car is built incrementally. First, the workers weld a chassis together, then they bolt on the engine block, and then they add the seats, the skin, and the tires. They paint the car, add the dashboard, and finally install the upholstery.

There are two reasons why you can build only incrementally, and not iteratively. First, building iteratively is horrendously wasteful and difficult (turning a motorcycle into a car is much more difficult than just building a car). Second, and much more importantly, the intermediate iterations do not have any business value. If the customer wants a car to take the kids to school, what would the customer do with a motorcycle and why should the customer pay for it?

Building incrementally also allows you to accommodate constraints on your time and budget. If you design a four-story dream house but you can afford only a single-floor house, you have two options. The first option is still build a four-story house with a single-floor budget by using plywood for all walls, sheet plastic for windows, buckets for a bathroom, dirt for floors, and a thatched roof. The second option is to properly build just the first floor of the four-story house. When you accumulate additional funds, you can then construct the second and third floors. A decade later, when you finally complete the building, that construction still matches the original architecture.

The ability to build incrementally over time, within the confines of the architecture, is predicated on the architecture remaining constant and true. With functional decomposition, you face ever-shifting piles of debris. It is fair to assume that those who know only functional decomposition are condemned to iterative construction. With volatility-based decomposition, you have a chance of getting it right.

Extensibility

The vertical slices of the system also enable you to accommodate extensibility. The correct way of extending any system is not by opening it up and hammering on existing components. If you have designed correctly for extensibility, you can mostly leave existing things alone and extend the system as a whole. Continuing the house analogy, if you want to add a second floor to a single-story house at some point in the future, then the first floor must have been designed to carry the additional load, the plumbing must have been done in a way that could be extended to the second floor, and so on. Adding a second floor by destroying the first floor and then building new first and second floors is called rework, not extensibility. The design of a Method-based system is geared toward extensibility: Just add more of these slices or subsystems.

About Microservices

I am credited as one of the pioneers of microservices. As early as 2006, in my speaking and writing I called for building systems in which every class was a service.2,3 This requires the use of a technology that can support such granular use of services. I extended Windows Communication Foundation (WCF) at the time to do just that, taking every class and treating it as a service while maintaining the conventional programming model of classes.4 I never called these services “microservices.” Then, as now, I did not think the microservices concept existed. There are no microservices—only services. For example, the water pump in my car provides a critical service to my car, and that pump is only 8 inches long. The water pump that the local water company uses to push water to my town provides the town with a very valuable service, but it is 8 feet long. The existence of a larger pump does not suddenly transform the pump in my car into a micropump: It is still just a pump. Services are services regardless of their size. To understand the origin of the artificial concept of microservices, you have to reflect on the history of service-orientation.

2. https://wikipedia.org/wiki/Microservices#History

3. Juval Löwy, Programming WCF Services, 1st ed. (O’Reilly Media, 2007), 543–553.

4. Löwy, Programming WCF Services, 1st ed., pp. 48–51; Juval Löwy, Programming WCF Services, 3rd ed. (O’Reilly Media, 2010), 74–75.

History and Concerns

At the dawn of service-orientation, in the early 2000s, many organizations simply exposed their system as a whole as a service. The resulting monstrous monolith was impossible to maintain and extend due to its complexity. Some 10 years of agony later, the industry recognized the error of this approach and started calling for more granular use of services, which it dubbed microservices. In common usage, microservices correspond to domains or subsystems—that is, to the slices (red boxes) of Figure 3-8. There are three problems with this idea as practiced today.

The first problem is the implied constraint on the number of services. If smaller services are better than larger services, why stop at the subsystem level? The subsystem is still too big as the most granular service unit. Why not have the building blocks of the subsystem be services? You should push the benefits of services as far down the architecture as possible. In a Method subsystem, the Manager, Engine, and ResourceAccess components within a subsystem must be services as well.

The second problem is the widespread use of functional decomposition in microservice design by the industry at large. This factor alone will doom every nascent microservices effort. Those attempting to construct microservices will have to contend with the complexity of both functional decomposition and service-orientation without gaining any of the benefits of the modularity of the services. This double punch may be more than what most projects can handle. Indeed, I fear that microservices will be the biggest failure in the history of software. Maintainable, reusable, extensible services are possible—just not in this way.

The third problem relates to communication protocols. Although the choice of communication protocols has more to do with detailed design than with architecture, the effect of the choice is worth a passing comment here. The vast majority of microservice stacks (as of this writing) use REST/WebAPI and HTTP to communicate with the services. Most technology vendors and consultants endorse this practice across the board (perhaps because it makes their life easier if everyone uses the lowest common denominator). These protocols, however, were designed for publicly facing services, as the gateway to systems. As a general principle, in any well-designed system you should never use the same communication mechanism both internally and externally.

For example, my laptop has a drive that provides it with a very important service: storage. The laptop also consumes a service offered by the network router for all DNS requests, and an SMTP server that offers email service. For the external services, the laptop uses TCP/IP; for the internal services like the drive, it uses SATA. The laptop utilizes multiple such specialized internal protocols to perform its essential functions.

Another example is the human body. Your liver provides you with a very important service: metabolism. Your body also provides a valuable service to your customers and organization, and you use a natural language (English) to communicate with them. However, you do not speak English to communicate with your liver. Instead, you use nerves and hormones.

The protocol used for external services is typically low bandwidth, slow, expensive, and error prone. Such attributes indicate a high degree of decoupling. Unreliable HTTP may be perfect for external services, but this protocol should be avoided between internal services where the communication and the services must be impeccable.

Using the wrong protocol between services can be fatal. It is not the end of the world if you cannot talk with your boss or have a misunderstanding with a customer, but you will die if you cannot communicate correctly or at all with your liver.

Similar level-of-service issues exist with specialization and efficiency. Using HTTP between internal services is akin to using English to control your body’s internal services. Even if the words were perfectly heard and understood, English lacks the adaptability, performance, and vocabulary required for describing the internal services’ interactions.

Internal services such as Engines and ResourceAccess should rely on fast, reliable, high-performance communication channels. These include TCP/IP, named pipes, IPC, Domain Sockets, Service Fabric remoting, custom in-memory interception chains, message queues, and so on.

Open and Closed Architectures

Any layered architecture can have one of two possible operational models: open or closed. This section contrasts the two alternatives. From this discussion, you can glean some additional design guidelines in the context of the classification of services.

Open Architecture

In an open architecture, any component can call any other component regardless of the layer in which the components reside. Components can call up, sideways, and down as much as you like. Open architectures offer the ultimate flexibility. However, an open architecture achieves that flexibility by sacrificing encapsulation and introducing a significant amount of coupling.

For example, imagine in Figure 3-4 that the Engines directly call the Resources. While such a call is technically possible, when you wish to switch Resources or merely change the way you access a Resource, suddenly all your Engines must change. How about the Clients calling ResourceAccess services directly? While that is not as bad as calling the Resources themselves, all the business logic must migrate to the Clients. Any change to the business logic would then force reworking the Clients.

Calling up a layer is also inadvisable. In Figure 3-4, what if a Manager called a Client to update some control in the UI? Now as the UI changes, the Manager must respond to that change, too. You have imported the volatility of the Clients to the Managers.

Calling sideways (intra-layer) also creates an inordinate amount of coupling. Imagine Manager A calling Manager B in Figure 3-4. In this case, Manager B is just an activity inside a use case executed by Manager A. Managers are supposed to encapsulate a set of independent use cases. Are the use cases of Manager B now independent of those of Manager A? Any change to Manager B’s way of doing the activity will break Manager A, calling to mind the issues of Figure 2-5. Calling sideways in this way is almost always the result of functional decomposition at the Managers level.

How about Engine A calling Engine B? Was Engine B a separate volatile activity from Engine A? Again, functional decomposition is likely behind the need to chain the Engines calls.

When using open architecture, there is hardly any benefit of having architectural layers in the first place. In general, in software engineering, trading encapsulation for flexibility is a bad trade.

Closed Architecture

In a closed architecture, you strive to maximize the benefits of the layers by disallowing calling up between layers and sideways within layers. Disallowing calling down between layers would maximize the decoupling between the layers but produce a useless design. A closed architecture opens a chink in the layers, allowing components in one layer to call those in the adjacent lower layer. The components within a layer are of service to the components in the layer immediately above them, but they encapsulate whatever happens underneath. Closed architecture promotes decoupling by trading flexibility for encapsulation. In general, that is a better trade than the other way around.

Semi-Closed/Semi-Open Architecture

It is easy to point out the clear problems with open architectures—of allowing calling up, down, or sideways. However, are all three sins equally bad? The worst of them is calling up: That not only creates cross-layer coupling, but also imports the volatility of a higher layer to the lower layers. The second worst offender is calling sideways because such calls couple components inside the layer. The closed architecture allows calling one layer below, but what about calling multiple layers down? A semi-closed/semi-open architecture allows calling more than one layer down. This, again, is a trade of encapsulation for flexibility and performance and, in general, is a trade to avoid.

Notably, the use of semi-closed/semi-open architecture is justified in two classic cases. This first case occurs when you design some key piece of infrastructure, and you must squeeze every ounce of performance from it. In such a case, transitioning down multiple layers may adversely affect performance. For example, consider the Open Systems Interconnection (OSI) model of seven layers for network communication.5 When vendors implement this model in their TCP stack, they cannot afford the performance penalty incurred by seven layers for every call, and they sensibly choose a semi-closed/semi-open architecture for the stack. The second case occurs within a codebase that hardly ever changes. The loss in encapsulation and the additional coupling in such a codebase is immaterial because you will not have to maintain the code much, if at all. Again, a network stack implementation is a good example for code that hardly ever changes.

5. https://en.wikipedia.org/wiki/OSI_model

Semi-closed/semi-open architectures do have their place. Nevertheless, most systems do not have the level of performance required to justify such designs, and their codebase is never that immutable.

Relaxing the Rules

For real-life business systems, the best choice is always a closed architecture. The discussion in the previous sections of the open and semi-open options should discourage you from any other choice.

While closed architecture systems are the most decoupled and the most encapsulated, they are also the least flexible. This inflexibility could lead to Byzantine-like levels of complexity due to the indirections and intermediacy, and rigid design is inadvisable. The Method relaxes the rules of closed architecture to reduce complexity and overhead without compromising encapsulation or decoupling.

Calling Utilities

In a closed architecture, Utilities pose a challenge. Consider Logging, a service used for recording run-time events. If you classify Logging as a Resource, then the ResourceAccess can use it, but the Managers cannot. If you place Logging at the same level as the Managers, only the Clients can log. The same goes for Security or Diagnostics—services that almost all other components require. In short, there is no good location for Utilities among the layers of a closed architecture. The Method places Utilities in a vertical bar on the side of the layers (see Figure 3-4). This bar cuts across all layers, allowing any component in the architecture to use any Utility.

You may see attempts by some developers to abuse the utilities bar by christening as a Utility any component they wish to short-circuit across all layers. Not all components can reside in the utilities bar. To qualify as a Utility, the component must pass a simple litmus test: Can the component plausibly be used in any other system, such as a smart cappuccino machine? For example, a smart cappuccino machine could use a Security service to see if the user can drink coffee. Similarly, the cappuccino machine may want to log how much coffee the office workers drink, have diagnostics, and be able to use the Pub/Sub service to publish an event notifying that it is running low on coffee. Each of these needs justifies encapsulation in a Utility service. In contrast, you will be hard-pressed to explain why a cappuccino machine has a mortgage interest calculating service as a Utility.

Calling ResourceAccess by Business Logic

This next guideline may be implied, but it is important enough to state explicitly. Because they are in the same layer, both Managers and Engines can call ResourceAccess services without violating the closed architecture (see Figure 3-4). Allowing Managers to call ResourceAccess is also implied from the section defining Managers and Engines. A Manager that uses no Engines must be able to access the underlying Resources.

Managers Calling Engines

Managers can directly call Engines. The separation between Managers and Engines is almost at the detailed design level. Engines are really just an expression of the Strategy design pattern6 used to implement the activities within the Managers’ workflows. Therefore, Manager-to-Engine calls are not truly sideways calls, as is the case with Manager-to-Manager calls. Alternatively, you can think of Engines as residing in a different or orthogonal plane to the Managers.

6. Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).

Queued Manager-to-Manager

While Managers should not call directly sideways to other Managers, a Manager can queue a call to another Manager. There are actually two explanations—a technical one and a semantic one—why this does not violate the closed architecture principle.

The technical explanation involves the very mechanics of a queued call. When a client calls a queued service, the client interacts with a proxy to the service, which then deposits the message into a message queue for the service. A queue listener entity monitors the queue, detects the new message, picks it off the queue, and calls the service. Using The Method structure, when a Manager queues a call to another Manager, the proxy is a ResourceAccess to the underlying Resource, the queue; that is, the call actually goes down, not sideways. The queue listener is effectively another Client in the system, and it is also calling downward to the receiving Manager. No sideways call actually takes place.

The semantic explanation involves the nature of use cases. Business systems quite commonly have one use case that triggers a latent, much-deferred execution of another use case. For example, imagine a system in which a Manager executing a use case must save some system state for analysis at the end of the month. Without interrupting its flow, the Manager could queue the analysis request to another Manager. The second Manager could dequeue at the month’s end and perform its analysis workflow. The two use cases are independent and decoupled on the timeline.

Opening the Architecture

Even with the best set of guidelines, time and again you will find developers trying to open the architecture by calling sideways, calling up, or committing some other violation of a closed architecture. Do not brush these transgressions aside or demand blind compliance with the guidelines. Nearly always, the discovery of such a transgression indicates some underlying need that made developers violate the guidelines. You must address that need correctly, in a way that complies with the closed architecture principle. For example, suppose that during design or code review, you discover one Manager directly calling another Manager. The developer may attempt to justify the sideways call by pointing out some requirement for another use case to execute in response to the original use case. It is very unlikely, however, that the response of the second Manager must occur immediately. Queuing the inter-Manager call would both be a better design and avoid the sideways call.

In another review, suppose you detect a Manager calling up to a Client—a gross violation of the closed architecture principle. As justification, the developer points to a requirement to notify the Client when something happens. While a valid requirement, calling up is not an acceptable resolution. Over time, other Clients could need the notification, or other Managers might need to notify the Client. What you have unearthed is volatility in who notifies and volatility in who receives that event. You should encapsulate that volatility by using a Pub/Sub service from the utilities bar. The Manager can, of course, call that Utility. In the future, adding other subscribing Clients or publishing Managers is a trivial task and would have no undesired repercussions in the system.

Design “Don’ts”

With the definitions in place for both the services and the layers, it is also possible to compile a list of things to avoid—the design “don’ts.” Some of the items on the list may appear obvious to you after the previous sections, yet I have seen them often enough to conclude they were not obvious after all. The main reason people go against a “don’t” guideline is that they have created a functional decomposition and have managed to convince themselves that it is not functional.

If you do one of the things on this list, you will likely live to regret it. Treat any violation of these rules as a red flag and investigate further to see what you are missing:

  • Clients do not call multiple Managers in the same use case. Doing so implies that the Managers are tightly coupled and no longer represent separate families of use cases, or separate subsystems, or separate slices. Chained Manager calls from the Client indicate functional decomposition, requiring the Client to stitch the underlying functionalities together (see Figure 2-1). Clients can call multiple Managers but not in the same use case; for example, a Client can call Manager A to perform use case 1 and then call Manager B to perform use case 2.

  • Clients do not call Engines. The only entry points to the business layer are the Managers. The Managers represent the system, and the Engines are really an internal layer implementation detail. If the Clients call the Engines, use case sequencing and associated volatility are forced to migrate to the Clients, polluting them with business logic. Calls from Clients to Engines are the hallmark of a functional decomposition.

  • Managers do not queue calls to more than one Manager in the same use case. If there are two Managers receiving a queued call, why not a third? Why not all of them? The need to have two (or more) Managers respond to a queued call is a strong indication that more Managers (and maybe all of them) would need to respond, so you should use a Pub/Sub Utility service instead.

  • Engines do not receive queued calls. Engines are utilitarian and exist to execute a volatile activity for a Manager. They have no independent meaning on their own. A queued call, by definition, executes independently from anything else in the system. Performing just the activity of an Engine, disconnected from any use case or other activities, does not make any business sense.

  • ResourceAccess services do not receive queued calls. Very similar to the Engines guideline, ResourceAccess services exist to service a Manager or an Engine and have no meaning on their own. Accessing a Resource independently from anything else in the system does not make any business sense.

  • Clients do not publish events. Events represent changes to the state of the system about which Clients (or Managers) may want to know. A Client has no need to notify itself (or other Clients). In addition, knowledge of the internals of the system is often required to detect the need to publish an event—knowledge that the Clients should not have. However, with a functional decomposition, the Client is the system and needs to publish the event.

  • Engines do not publish events. Publishing an event requires noticing and responding to a change in the system and is typically a step in a use case executed by the Manager. An Engine performing an activity has no way of knowing much about the context of the activity or the state of the use case.

  • ResourceAccess services do not publish events. ResourceAccess services have no way of knowing the significance of the state of the Resource to the system. Any such knowledge or responding behavior should reside in Managers.

  • Resources do not publish events. The need for the Resource to publish events is often the result of a tightly coupled functional decomposition. Similar to the case for ResourceAccess, business logic of this kind should reside in Managers. As a Manager modifies the state of the system, the Manager should also publish the appropriate events.

  • Engines, ResourceAccess, and Resources do not subscribe to events. Processing an event is almost always the start of some use case, so it must be done in a Client or a Manager. The Client may inform a user about the event, and the Manager may execute some back-end behavior.

  • Engines never call each other. Not only do such calls violate the closed architecture principle, but they also do not make sense in a volatility-based decomposition. The Engine should have already encapsulated everything to do with that activity. Any Engine-to-Engine calls indicate functional decomposition.

  • ResourceAccess services never call each other. If ResourceAccess services encapsulate the volatility of an atomic business verb, one atomic verb cannot require another. This is similar to the rule that Engines should not call each other. Note that a 1:1 mapping between ResourceAccess and Resources (every Resource has its own ResourceAccess) is not required. Often two or more Resources logically must be joined together to implement some atomic business verbs. A single ResourceAccess service should perform the join rather than inter-ResourceAccess services calls.

Strive for Symmetry

Another universal design rule is that all good architectures are symmetric. Consider your own body. You do not have a third hand sticking up on your right side because evolutionary pressures were omnidirectional, enforcing symmetry. Evolutionary pressures apply to software systems as well, forcing the systems to respond to the changing environment or become extinct. The quest for symmetry, however, is only at the architecture level, not in detailed design. Certainly, your internal organs are not symmetric because such symmetry offered no evolutionary advantage to your ancestors (i.e., the system dies when you expose its internals).

The symmetry in software systems manifests in repeated call patterns across use cases. You should expect symmetry, and its absence is a cause for concern. For example, suppose a Manager implements four use cases, three of which publish an event with the Pub/Sub service and the fourth of which does not. That break of symmetry is a design smell. Why is the fourth case different? What are you missing or overdoing? Is that Manager a real Manager, or is it a functionally decomposed component without volatility? Symmetry can also be broken by the presence of something, not just by its absence. For example, if a Manager implements four use cases, of which only one ends up with a queued call to another Manager, that asymmetry is also a smell. Symmetry is so fundamental for good design that you should generally see the same call patterns across Managers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset