Chapter 5 – Taming the Beast
by kirk knoernschild
Modularity is not a new concept. In his 1972 paper titled, “On the Criteria to be used in Decomposing Systems Into Modules”, David Parnas cited the work of Gouthier and Pont as the first lucid statement of modular programming:
A well-defined segmentation of the project effort ensures system modularity. Each task forms a separate, distinct program module. At implementation time each module and its inputs and outputs are well-defined, there is no confusion in the intended interface with other system modules. At checkout time the integrity of the module is tested independently; there are few scheduling problems in synchronizing the completion of several tasks before checkout can begin. Finally, the system is maintained in modular fashion; system errors and deficiencies can be traced to specific system modules, thus limiting the scope of detailed error searching.
Parnas would go on to discuss the important decisions developers must make when modularizing their software systems. Encapsulating design decisions within autonomous modules allowed for modules to evolve independently and reveal little about their inner workings. The designs were encapsulated, shielding other developers from this unnecessary complexity, and helping them to more easily understand the software system.
While a lot has changed since the Parnas paper was first published, one constant remains – software is still incredibly difficult to design and develop. Unfortunately, in the decades that have passed since modular programming was first introduced, these ideas were lost. Today, few of us consciously decompose our software systems into modules. Fortunately, we have the luxury of using a platform with an excellent unit of modularity built in, and it’s time we start using it.
5.1 – Enterprise Complexity
Most software systems don’t begin their life as complex entities, but grow complex over time. Lehman’s 2nd law summarizes this phenomenon:
As a system evolves, it’s complexity increases unless work is done to maintain or reduce it.
Lehman chose to use the evolution of IBM’s OS/360 for his work by examining the growth of the system over a series of releases. His findings revealed that for each release, the size of the software system increased. The logical conclusion? As software system size increases, so too does the complexity.
Many of us have experienced this phenomenon. In almost every way, larger software systems are inherently more difficult to design, build, and manage than are smaller software systems. Examining more modern evidence of Lehman’s work, we turn to the evolution of the Spring framework. The diagram in Figure 1 illustrates this evolution between release 0.9.1 and release 2.5. Since the earliest releases of the framework dating back to 2003, we’ve seen almost a six-fold increase in the size of the code. Examining other projects, such as Linux, Tomcat, or FireFox reveals similar growth.

Figure 1: Evolutionary Growth of Spring Framework in Lines of Code
Such phenomenal growth is not only expected, but desired. We can only hope that the systems we create are in such high demand that we have the opportunity to grow them over time. A software system that doesn’t change, dies. In fact, studies (http://users.jyu.fi/~koskinen/smcosts.htm) suggest that the cost to maintain software and manage its evolution exceeds 90% of the total cost of a system. Evolution is a big deal!
So evolution is not only expected, but desirable. It’s in our best interests to encourage change in software systems. But as we’ve seen, change breeds complexity. What is the cause of all this complexity, and what can we do to manage this complexity?
5.2 – Technical Debt
Software tends to rot over time. When you establish your initial vision for the software’s design and architecture, you imagine a system that is easy to modify, extend, and maintain. Unfortunately, as time passes, changes trickle in that exercise your design in unexpected ways. Each change begins to resemble nothing more than another hack, until finally the system becomes a tangled web of code that few developers care to venture through. Sadly, design rot is self-inflicted, and technical debt describes the affect of rotting design.
Technical debt is a metaphor developed by Ward Cunningham to describe the design trade-off’s made in order to meet schedules and customer expectations. Martin Fowler offers the following analogy when comparing technical debt to financial debt.
Like a financial debt, the technical debt incurs interest payments, which come in the form of the extra effort that we have to do in future development because of the quick and dirty design choice. We can choose to continue paying the interest, or we can pay down the principal by refactoring the quick and dirty design into the better design. Although it costs to pay down the principal, we gain by reduced interest payments in the future.
There are situations where leveraging suboptimal designs, thereby incurring technical debt, is warranted to meet short term demands. For instance, the schedule may not not allow longer term designs to be used. However, if we ignore this debt, it continues to build over time and incurring too much debt leads to significant inefficiencies surrounding our ability to effectively change the software system. Lehman’s law in action, for sure. In fact, at some point we hit a wall where we are no longer able to afford the interest payments. It’s imperative that we manage technical debt to ensure long term survivability of our software system. Too much debt will cause the system to crumble. At some point, we must deal with the technical debt we’ve incurred. If we don’t, our design will continue to rot!
5.3 – Design Rot
The most common cause of rotting software is tightly coupled code with excessive dependencies. Of course, any interesting software system must have dependencies for it to do anything. Yet for large teams and large applications, managing dependencies is especially important. Excessive dependencies increase technical debt and cause numerous problems. And as Lehman’s law suggests, the problems will persist and likely increase unless work is done to maintain it.
5.3.1 – Hinder Maintenance
Dependencies hinder the maintenance effort. When you’re working on a system with heavy dependencies, you typically find that changes in one area of the application trickle to many other areas of the application. In some cases, this cannot be avoided. For instance, when you add a column to a database table that must be displayed on the user interface, you’ll be forced to modify at least the data access and user interface layers. Such a scenario is mostly inevitable. However, applications with a well thought dependency structure make change easier since developers have a more complete understanding of the impact of change.
5.3.2 – Prevent Extensibility
Dependencies prevent extensibility. The goal of flexible software architecture is to remain open for extension but closed to modification. The desire is to add new functionality to the system by extending existing abstractions, and plugging these extensions into the existing system without making rampant modifications. One reason for heavy dependencies is the improper use of abstraction, and those cases where abstractions are not present are areas that are difficult to extend. Abstraction aids large teams by exposing well-defined extension points, as well as encapsulating implementation complexity.
5.3.3 – Inhibit Reusability
Dependencies inhibit reusability. Reuse is often touted as a fundamental advantage of well-designed object oriented software. Unfortunately, few applications realize this benefit. Too often, we emphasize class level reuse. To achieve higher levels of reuse, careful consideration must also be given to the package structure and deployable unit structure. Software with complex package and physical dependencies minimize the likelihood of achieving higher degrees of reuse.
5.3.4 – Restrict Testability
Dependencies restrict testability. Tight coupling between classes eliminates the ability to test classes independently. Unit testing is a fundamental principle that should be employed by all developers. Tests provide you the courage to improve your designs, knowing flaws will be caught by unit tests. They also help you design proactively and discourage undesirable dependencies. Heavy dependencies do not allow you to test software modules independently. Teams with few tests cannot respond to change easily due to the inability to access the impact of change.
5.3.5 – Hamper Integration
Dependencies hamper integration. It’s easy for separate teams to plow forward, usually under tremendous pressure from looming deadlines. They operate under the false assumption that if they can simply reach the final feature destination, they can quickly pull things together toward the end of a project. This Big Bang approach to integration does not work. As individual modules are pulled together, common issues that surface include degradation of overall system performance, incorrect levels of behavioral granularity provided by system modules, and transactional incompatibilities. More frequent integration brings many of these issues to the forefront earlier in the project.
5.3.6 – Limit Understanding
Dependencies limit understanding. When working on a software system, it’s important that you understand the system’s structural architecture and design constructs. A structure with complex dependencies is inherently more difficult to understand. Clear and concise dependencies that are well-thought allow teams to more easily access the impact of change.
5.4 – Cyclic Dependencies – The Death Knell
Excessive dependencies are bad. But cyclic dependencies are especially bad. Cyclic dependencies are manifest in various ways at different levels within a system. It’s also possible that acyclic relationships at one level cause cycles at another. For more information on cyclic and acyclic dependencies, be sure to check out the Acyclic Relationships pattern.
5.4.1 – Types of Cycles
Cycles exist across a variety of entities; notably class, package and modules. Class cycles exist when two classes, such as Customer and Bill shown in Figure 2, each reference the other. In this example, we can assume a Customer has a list of Bill instances, and Bill references the Customer to calculate a discount amount. This is also known as a bi-directional association. It’s a maintenance and testing issue, since you can’t do anything to either class without affecting the other.

Figure 2: Cyclic Dependency Between Classes
The code in Listing 1 shows the customer class while the code in Listing 2 shows the Bill class. Certain portions of each class have been omitted to improve clarity. The bi-directional relationship can be clearly identified.
package com.kirkk.cust;
import java.util.*;
import java.math.BigDecimal;
import com.kirkk.bill.*;
public class Customer {
private List bills;
//Derivation of the discount depends on the number of bills for a specific Customer.
public BigDecimal getDiscountAmount() {
if (bills.size() > 5) {
return new BigDecimal(0.1);
} else {
return new BigDecimal(0.03);
}
}
public void createBill() {
Bill bill = new Bill(this);
if (bills == null) {
bills = new ArrayList();
}
bills.add(bill);
}
}
Listing 1: The Customer Class
package com.kirkk.bill;
import com.kirkk.cust.*;
import java.math.BigDecimal;
public class Bill {
private Customer customer;
public Bill(Customer customer) {
this.customer = customer;
}
public BigDecimal pay() {
BigDecimal discount = new BigDecimal(1).subtract(this.customer.getDiscountAmount()).setScale(2, BigDecimal.ROUND_HALF_UP);
//Apply the discount and make the payment...
return paidAmount;
}
}
Listing 2: The Bill Class
Class cycles can be broken a few different ways, one of which is to introduce an abstraction that breaks the cycle, as shown in Figure 3. Now, the Bill class can easily be tested with a mock DiscountCalculator. Testing Customer, of course, still requires the presence of Bill. This is not a cyclic issue, it’s a different type of coupling issue because Bill is a concrete class, and is fodder for a separate discussion, which we’ll tackle when discussing the Manage Relationships pattern. Clearly, introducing DiscountCalculator has broken the cycle between Customer and Bill…but has it broken all cycles, including those that might exist between modules?

Figure 3: Breaking the Cycle
Listing 3 shows the modified Bill class that implements the DiscountCalculator interface, which is shown in Listing 4.
<pre>package com.kirkk.bill;
import java.math.BigDecimal;
public class Bill implements DiscountCalculator {
private DiscountCalculator calculator;
public Bill(DiscountCalculator calculator) {
this.calculator = calculator;
}
public BigDecimal pay() {
BigDecimal discount = new BigDecimal(1).subtract(this.calculator.getDiscountAmount()).setScale(2, BigDecimal.ROUND_HALF_UP);
//Apply the discount and make the payment...
return paidAmount;
}
}
Listing 3: The Modified Bill Class
package com.kirkk.bill;
import java.math.BigDecimal;
public interface DiscountCalculator {
public BigDecimal getDiscountAmount();
}
Listing 4: The DiscountCalculator Interface
5.4.2 – Creeping Cycles
We don’t intentionally create cyclic dependencies between modules. Instead, they tend to creep into our design. They commonly surface when cyclic or acyclic relationships at one level cause cycles at another. For instance, if Customer and DiscountCalculator are placed in a customer.jar module, and Bill is placed in a billing.jar module, a cyclic dependency between the customer.jar and billing.jar modules exists even though the bi-directional relationships between classes has been broken. The cyclic dependencies between modules is shown in Figure 4. Allocating the Customer and Billing classes to the customer.jar and billing.jar modules is the cause of the cycle.

Figure 4: Cycles Between Modules
To break the cycle, we must move DiscountCalculator to its own module, or the billing module. Figure 5 illustrates that moving the DiscountCalculator to the billing.jar module has broken the cyclic dependency between the modules. Obviously this is a trivial example. For larger software systems with potentially thousands of classes and numerous modules to manage, the challenge is much greater. This is where the patterns in this book and first class support for modularity on the Java platform will serve as a valuable aid.

Figure 5: Acyclic Between Modules
5.4.3 – Managing Cycles
Fortunately, there are many ways (some easier than others) to manage dependencies and eliminate cycles. Test Driven Development is a great way to manage class cycles assuming we strive to test classes in isolation. JDepend allows you to manage package cycles, either by writing package constraint tests or including JDepend reports within your Ant build script. Cycles between modules can be managed using a Levelized Build, where individual modules are built, including only necessary modules in the build class path. Additional patterns are useful in managing and massaging module dependencies.
JarAnalyzer can also be included in your build script, generating a component diagram illustrating the relationship between JAR files, or a dependency report similar to that of JDepend. Maven and Ivy also provide ways to help manage dependencies. And of course, looming on the horizon is OSGi, which will offer runtime support for modularity and will enforce module dependencies.
5.4.4 – But Are Cycles Always Bad?
Generally speaking, cycles are always bad! But some cycles are worse than others. Cycles among classes are tolerable, assuming they don’t cause cycles among the packages or modules containing them (ie. the classes must be in the same module, essentially encapsulating the design). For a discussion on encapsulating design, see Chapter 4, Section 4.4. Cycles among packages may also be tolerable, assuming they don’t cause cycles among the modules files containing them (again, packages are in the same module). Most important is that we are aware of the relationships existing between the modules. In so many cases, we aren’t.
5.5 – In The Real World
On a project, we were integrating with a vendor product. The vendor product would help us fulfill the needs to the business processing by applying industry standard coding that helped us comply with federal mandates. The relationship with the vendor was volatile, and negotiation with an alternative vendor was underway.
Unfortunately, our project deadline didn’t align well with the vendor contract. We knew we’d have a production release about three months before the vendor decision would be made. In light of this, we encapsulated all vendor code into a single module, and separated out the interface classes to a separate module by applying the Separate Abstractions pattern. When the decision was finally made to establish a relationship with the new vendor, making the switch was less painful. We created a new module that integrated with the new vendor and implemented the interfaces. The ripple effect throughout the system was minimal, and we were able to easily test the integration independently. We’ll see this example play out in front of us when we walk through the reference implementation in Chapter 7.
5.6 – Joints, Modules, & SOLID
It’s imperative to manage dependencies between our modules. The SOLID principles of object-oriented design help minimize class coupling and increase class cohesion, making software easier to maintain and extend. Yet there is also a cost to applying the principles. The additional layers of abstraction increase the complexity of the system. Blindly applying the principles to all classes within a system can actually decrease our ability to maintain and extend the software. Remember the paradox discussed in Chapter 4, SEction 4.3.1 – with flexibility comes complexity. This begs the question, “Where is the most appropriate place to apply the SOLID principles?”
We touched briefly on the joints of the system in Chapter 4, Section 4.4. Every system has joints, which is the point where two modules connect. It is these joints within the system that require the greatest flexibility and resiliency. The reason for this is driven by change. Change that is encapsulated within a single module poses less threat and risk than change that ripples across many modules. This is trivial, and it’s why changing the private methods of a class are easier than changing the public methods. Likewise, change confined to a single module is easier than change that spans modules. That’s logical. So if we’re able to create well-designed modules, it makes our life a little bit easier. And using the SOLID design principles, and other proven design patterns, allows us to bring greater modularity to our applications if we apply the principles when designing our modules.
It is these joints, or module boundaries, within a system that represent the ideal opportunity to leverage the SOLID principles. Using the SOLID principles to minimize coupling between modules offers the greatest opportunity and reward. For example, it’s not realistic to design an entire system open for extension but closed to modification (the Open Closed Principle). But we can use OCP at the module boundaries (aka. the joints) to create modules with enough design flexibility to confine change to a single module. Using the SOLID principles when designing the relationships between modules provides a low barrier point of entry for using these principles to create better designs. For instance, it’s much easier to refactor the classes if we know the refactoring is confined to a single module. With a rich suite of unit tests, we can prove it has no impact to the outside world. But if we have to refactor the classes and the refactoring spans the joints of the system, well…that’s an architectural refactoring. It gets difficult and messy.
This is a major reason why frameworks such as OSGi, and the patterns in this book are so important – they make it just a little bit easier to design really big software systems that we can understand, maintain, and extend. And if we apply the SOLID principles in conjunction with the module patterns, we have guidance on how to best design large software systems. As interest in designing more modular software continues to increase, and especially as OSGi continues to gain momentum, the module design patterns will grow in importance in helping us manage complexity, and new patterns will also likely emerge.
5.7 – Managing Complexity
Many of the benefits of modularity can be gleaned from the definition of a software module. Deployable! Manageable! Reusable! Composable! Indeed, they are substantial benefits. But the greatest benefit of modularity is that it helps us tame the complexity of a software system as it evolves. By taming complexity, it’s much easier to understand the impact of change within large software systems. Since 90% of the total cost of a system involves making change to an existing system, this is a substantial benefit. Modularity is the work we do to help manage the complexity of a system as it evolves. Time for an example that illustrates this benefit.
5.7.1 – Illustrating the Benefit
The diagram in Figure 6 illustrates the benefit of modularity. The top left quadrant shows a sample system with a relatively complex class structure. When change occurs within a single class, shown in red in the bottom left quadrant, understanding the impact of change is difficult. It appears possible that it can propagate to any class dependent on the class highlighted in red. Assessing the impact of change requires that we analyze the complete class structure. The ripple effect appears significant, and change instills fear.
But if the system is modular with classes allocated to these modules, as shown in the bottom right quadrant, then understanding the impact of change can be isolated to a discrete set of modules. And this makes it much easier to identify which modules contain classes that might also change, as shown in the top right quadrant. Change is isolated to classes within modules that are dependent on the module containing the class that is changing.
This is a simple example, but it serves as evidence of the need for modular architecture, and illustrates one reason why modularity is so important. Modularity makes understanding the system easier. It makes maintaining the system easier. And it makes reusing system modules much more likely. As systems grow in size and complexity, it’s imperative that we design more modular software. That means we need a module system for the Java platform. It means that module system shouldn’t be shielded from enterprise developers. And it means we need to understand the patterns that are going to provide the guidance necessary in helping us design more modular software.

Figure 6: Modules Make Understanding Change Easier
Of course, the object-oriented paradigm promised similar benefits, but experience has shown that objects are too fine-grained. Even a system of moderate size might be composed of thousands of classes, making it virtually impossible for any single person to fully understand the entire system. Does this undermine the benefits of the object-oriented paradigm? Certainly not. But more is needed to help manage the complexity of large enterprise software systems.
Modules are more coarse-grained units than objects that allow us to encapsulate complex design decisions. A module might contain many classes, but the design decisions surrounding the relationships between classes can be encapsulated within a module. Cohesive modules encapsulate behavior and expose it only through well-defined interfaces. Because modules are cohesive, change is isolated to the implementation details of a module. Module behavior that is exposed through interfaces ensures new modules containing alternative implementations can be developed without modifying existing modules. The result is a reduction in complexity that helps ease the maintenance effort. In general, a well-designed modular system is simply easier to understand.
Of course, modularity doesn’t guarantee these benefits any more than using object orientation guarantees the benefits of the object-oriented paradigm. It still requires that we design our modules correctly. We’ll explore these ideas further in the next chapter.
5.8 – Conclusion
Software complexity is our worst enemy. Complexity inhibits our ability to gracefully adapt software systems in response to changing demands. And change is good because it means our software systems are gaining acceptance. Modularity helps tame this complexity, but only if the software modules we create are of high quality. It’s as simple to create a mess using modules as it is with objects and services. This chapter helped illustrate the benefit of modularity. Subsequent chapters discuss how we can realize this benefit.