Download a PDF of this article
When you form a startup business, it’s a good strategy to spend money slowly, with the goal of generating revenue quickly. This is also a great one-line summary of the Agile method of software development. If time is your currency, spend it on only the most important tasks, and get something into production early to serve your users. This is well understood. But I still often hear the question, “Where does the software design phase fit into the Agile development process?” This is the topic I’ll explore here, especially as it relates to Java development.
With two-week or three-week sprints, it may seem challenging to find time for proper software architecture and design. To answer this challenge, I turn to a precursor of Agile, Extreme Programming (XP), as described in Kent Beck’s book, Extreme Programming Explained. In his book, Beck suggests a design strategy with goals that are aligned with both the process and the overall project goals. As far as I can see, Agile agrees with this strategy. (Read this fascinating interview with Beck, conducted by Andrew Binstock a few years ago.)
Just as it’s a myth that there’s little to no planning with the Agile development process, it’s a myth that Agile doesn’t account for design. For example, I’ve been part of more planning and retrospective meetings with projects that follow the Agile process than with any other process. Agile has enabled me to continually think about and consider software design with each sprint. This is quite the opposite of the waterfall software development process, where you consider design only once early in the process.
The notion of “sprint zero,” to me, is another myth. By definition, taking an arbitrary amount of time at the beginning of a project to design or build a backlog isn’t a sprint and runs counter to Agile. It’s effectively water-scrum-fall, a compromise.
There’s a better way to do Agile, and that’s to focus on simplicity.
Some things are inherently complex by their nature, but often complexity has simplicity at its base. The theory for design in Agile is to have just enough design to meet your immediate goals.
I believe in this approach, but I’ll admit there is a risk: If your design is too simple, technical debt can accumulate. There may be sprints where a concerted, collaborative effort is needed to make larger design decisions. However, it’s important to ensure that you’re always doing just the amount of design that’s needed.
Here are some strategies to employ; some are general, but some are Java-specific.
It’s best to have a design strategy that carries through to each sprint. The strategy should enforce design concepts that are in line with the project goals and are easily communicated to team members. I’m in favor of small bouts of design with each sprint to continually improve the project structure. However, design is not an ad hoc process. The following elements of a good design strategy should be reinforced with each sprint:
Good software design is key to long-term project success and to maintaining team morale, no matter the language or platform. Still, there are some approaches that apply particularly well to Java projects.
Encourage experimentation. Your Agile process needs to account for experimentation and even encourage it—not on every sprint, perhaps, but certainly experimentation can be helpful throughout the development lifecycle.
For example, the Java ecosystem is rich, and this means you may need to consider the use of one open source or commercial package among many, such as which NoSQL database to use or which storage paradigm to adopt. To make the best decisions, you might need to try the alternatives. This suggests dedicating a sprint, now and then, to conduct experiments.
That said, be realistic. Imagine a possible design decision to build your own NoSQL database for future flexibility. Experimenting with building a NoSQL database engine is likely not the best use of time.
Whereas past development practices stressed designing your own core capabilities to anticipate potential possibilities, Agile doesn’t necessarily support this. Something unanticipated may come along, such as the rise in graph databases, which better suits your application and data. If you built your own NoSQL database, switching means you have wasted all that effort. Or worse, making a big investment writing, debugging, and tuning a NoSQL database engine may delay the move to a graph database that’s perhaps a superior choice, due to an all-too-human desire to justify your prior investment.
Consider design by contract. Java interfaces are used to define contracts, and many developers know this. Contracts serve as a layer of abstraction, because they help break dependencies between discrete classes. In other words, if you write your code to interfaces, you can change the implementing classes without affecting those that use them. This is known as interface decoupling and is directly in line with Agile’s rule of simplicity, with a goal of limiting the side effects of code changes.
For example, say you are implementing an automated barista for a very busy coffee shop. There are different types of beans to use and different brewing methods depending on the order. You can write the Barista
class to use different classes directly, each representing the different coffee beans and makers. It is better to abstract the implementations using interfaces: CoffeeBean
and CoffeeMaker
.
Although the details within the classes are different, each class implements the same set of methods. Therefore, the correct classes can be supplied to the Barista
constructor, see the two lines that begin with the keyword new
.
public class Barista {
CoffeeBean coffeeBeans = null;
CoffeeMaker coffeeMaker = null;
public Barista(CoffeeBean beans, CoffeeMaker maker) {
// …
}
public boolean brew() {
coffeeMaker.brew( coffeeBeans );
}
}
// …
Barista barista = new Barista(
new EspressoBeans(),
new EspressoMaker() );
barista.brew();
The result is a system that’s easy to extend, sprint by sprint, to automate the creation of new coffee drinks.
Avoid cyclic dependencies. Interface decoupling can result in risky dependencies, especially when you’re building software in small, rapid changes.
For example, consider a project that uses four Java packages: A, B, C, and D. Package A has dependencies on packages B and C. Package B has a dependency on package C. Package D has a dependency on Package B (see Figure 1).
In this scenario, what if package C later adds a dependency on package D? You have a nasty cyclic dependency between them.
Figure 1. Cyclic dependency in Java packages
This scenario may seem contrived, but it’s more common than you might think in complex applications. It can result in build issues and side effects when making changes to a package and its classes. Cyclic dependencies can easily sneak up on you in an Agile project involving many developers, but proper design effort applied at every sprint will help to avoid it.
Apply the dependency inversion principle (DIP). With DIP, you create the right level of abstraction to avoid highly coupled classes yet allow them to be used together in ways that make sense to break cyclic dependencies.
Specifically, DIP says the following:
In the example in Thorben Janssen’s article on DIP, the familiar topic of coffee is used with the CoffeeBean
interface from the earlier code example. However, DIP is important when you consider how CoffeeBean
objects are used. They can be ground, brewed, or even eaten as snacks.
Sticking with the traditional notion of making drinkable coffee, you will need a CoffeeMaker
object that uses CoffeeBean
objects. Since there are many different types of coffeemakers, this application defines an interface also, as shown in Figure 2.
Figure 2. DIP, with strict relationship rules, avoids cyclic dependencies.
With DIP, it’s important to keep the relationships proper. Although CoffeeMaker
s depend on CoffeeBean
s, CoffeeBean
s should be written with no knowledge of the types of different CoffeeMaker
s, nor should beans be partial to how they’re roasted, ground, and otherwise prepared.
Implement scalability through tiers. Following simple design strategies so far will help you design for scale later, when you need that scale. With interfaces and DIP, you can easily insert tiers within your codebase as growth requires it without much rework.
For example, early in the implementation of a web application, you may choose JavaServer Pages that write directly to a database. As the project progresses, you might find it’s better to have a thin data tier to abstract your storage choice. As usage grows, you may choose to add yet another tier to offload transaction processing, and so on (see Figure 3).
Figure 3. An example of a typical multitiered web application
I find with a distributed architecture, such as components spread across tiers, using a form of messaging middleware helps. It allows you to reduce dependencies and remove tight coupling using anonymous publish-subscribe messaging, asynchronous communication, and reliability. You can use REST, Java Message Service, the Data Access Object pattern, WebSockets, a cloud service, or an edge protocol such as MQTT, for example. This approach helps you design, code, and deploy components independently, increasing your agility. (A useful overview of tiered software design is “Distributed multitiered applications.”)
You can follow an Agile development process yet still have time to do proper software design. Instead of completing large swaths of design work up front, you continually perform just enough design with each sprint for the problems to be solved at that moment. But as you’ve seen here, there’s still a need for a long-term design strategy, and the Java design practices presented help in the long term.
Eric J. Bruno is in the advanced research group at Dell focused on Edge and 5G. He has almost 30 years experience in the information technology community as an enterprise architect, developer, and analyst with expertise in large-scale distributed software design, real-time systems, and edge/IoT. Follow him on Twitter at @ericjbruno.
Previous Post
Next Post