Design Patterns and Best Practices in Java

4 (4 reviews total)
By Kamalmeet Singh , Adrian Ianculescu , Lucian-Paul Torje
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. From Object-Oriented to Functional Programming

About this book

Having a knowledge of design patterns enables you, as a developer, to improve your code base, promote code reuse, and make the architecture more robust. As languages evolve, new features take time to fully understand before they are adopted en masse. The mission of this book is to ease the adoption of the latest trends and provide good practices for programmers.

We focus on showing you the practical aspects of smarter coding in Java. We'll start off by going over object-oriented (OOP) and functional programming (FP) paradigms, moving on to describe the most frequently used design patterns in their classical format and explain how Java’s functional programming features are changing them.

You will learn to enhance implementations by mixing OOP and FP, and finally get to know about the reactive programming model, where FP and OOP are used in conjunction with a view to writing better code. Gradually, the book will show you the latest trends in architecture, moving from MVC to microservices and serverless architecture. We will finish off by highlighting the new Java features and best practices. By the end of the book, you will be able to efficiently address common problems faced while developing applications and be comfortable working on scalable and maintainable projects of any size.

Publication date:
June 2018


Chapter 1. From Object-Oriented to Functional Programming

The objective of this chapter is to introduce the reader to the fundamental concepts of writing robust, maintainable, and extendable code using design patterns and the latest features available in Java. In order to achieve our objective, we will cover the following topics:

  • What are programming paradigms?
  • Imperative paradigm
  • Declarative and functional paradigms
  • Object-oriented paradigm
  • An overview of Unified Modeling Language
  • Object-oriented design principles

Java – an introduction

In 1995, a new programming language was released, inspired by the well-known C++ and the lesser known Smalltalk. Java was the name of this new language, and it tried to fix most of the limitations its predecessors had. For example, one important feature of Java that made it popular was write once and run anywhere; that is, you could develop your code on a Windows machine and run it on a Linux or any other machine, all you needed was a JVM. It provided additional features such as garbage collection, which freed up the developer from needing to maintain memory allocation and deallocations; the Just in Time compiler (JIT) made Java intelligent and fast, and removing features such as pointers made it more secure. All the aforementioned features and the later addition of web support made Java a popular choice among developers. Around 22 years later, in a world where new languages come and disappear in a couple of years, Java version 10 has already been successfully launched and adapted by the community, which says a lot about the success of Java.


Java programming paradigms

What are programming paradigms? Since software development began, there have been different approaches to designing programing languages. For each programming language, we have a set of concepts, principles, and rules. Such a set of concepts, principles, and rules is called a programming paradigm. In theory, languages are considered to fall under one paradigm only, but, in practice, programming paradigms are mostly combined in one language. 

In the following section, we will highlight the programming paradigms on which Java programming language is based, along with the major concepts that describe these paradigms. These are imperative, object-oriented, declarative, and functional programming.


Imperative programming

Imperative programming is a programming paradigm in which statements are written to change the state of the program. This concept emerged at the beginning of computing and is very close to the computer's internal structure. The program is a set of instructions that is run on the processing unit, and it changes the state (which is stored as variables in the memory) in an imperative manner. The name imperative implies the fact that the instructions that are executed dictate how the program operates.

Most of the most popular programming languages today are based, more or less, on the imperative paradigm. The best example of a mainly imperative language is C.

Real-life imperative example

In order to better understand the concept of the imperative programming paradigm, let's take the following example: you're meeting a friend for a hackathon in your town, but he has no idea how to get there. We'll explain to him how to get there in an imperative way:

  1. From the Central Station, take tram 1.
  2. Get off the tram at the third station.
  3. Walk to the right, toward Sixth Avenue, until you reach the third junction.

Object-oriented paradigm

The object-oriented paradigm is often associated with imperative programming, but, in practice, both functional and object-oriented paradigms can coexist. Java is living proof that supports this collaboration.

In the following section, we will briefly highlight the main object-oriented concepts as they are implemented in the Java language.

Objects and classes

Objects are the main elements of an object-oriented programming (OOP) language. An object holds both the state and the behavior.

If we think of classes as a template, objects are the implementation of the template. For example, if human is a class that defines the behavior and properties that a human being can have, you and I are objects of this human class, as we have fulfilled all the requirements of being a human. Or, if we think of car as a class, a particular Honda Civic car will be an object of this class. It will fulfill all the properties and behaviors that a car has, such as it has an engine, a steering wheel, headlights, and so on, and it has behaviors of moving forward, moving backward, and so on. We can see how the object-oriented paradigm can relate to the real world. Almost everything in the real world can be thought of in terms of classes and objects, hence it makes OOP effortless and popular.

Object-oriented programming is based on four fundamental principles:

  • Encapsulation
  • Abstraction
  • Inheritance
  • Polymorphism (subtyping polymorphism).


Encapsulation basically means the binding of attributes and behaviors. The idea is to keep the properties and behavior of an object in one place, so that it is easy to maintain and extend. Encapsulation also provides a mechanism to hide unnecessary details from the user. In Java, we can provide access specifiers to methods and attributes to manage what is visible to a user of the class, and what is hidden.

Encapsulation is one of the fundamental principles of object-oriented languages. It helps in the decoupling of different modules. Decoupled modules can be developed and maintained more or less independently. The technique through which decoupled modules/classes/code are changed internally without affecting their external exposed behavior is called code refactoring.


Abstraction is closely related to encapsulation, and, to some extent, it overlaps with it. Briefly, abstraction provides a mechanism that exposes what an object does and hides how the object does what it's supposed to do.

A real-world example of abstraction is a car. In order to drive a car, we don't really need to know what the car has under the hood, but we need to know the data and behavior it exposes to us. The data is exposed on the car's dashboard, and the behavior is represented by the controls we can use to drive a car.


Inheritance is the ability to base an object or class on another one. There is a parent or base class, which provides the top-level behavior for an entity. Every subclass entity or child class that fulfills the criteria to be a part of the parent class can inherit from the parent class and add additional behavior as required.

Let's take a real-world example. If we think of a Vehicle as a parent class, we know a Vehicle can have certain properties and behaviors. For example, it has an engine, doors, and so on, and behavior-wise it can move. Now all entities that fulfill these criteria—for example, Car, Truck, Bike, and so on—can inherit from Vehicle and add on top of given properties and behavior. In other words, we can say that a Car is a type of Vehicle.

Let's see how this will look as code; we will first create a base class named Vehicle. The class has a single constructor, which accepts a String (the vehicle name):

public class Vehicle 
  private Stringname;
  public Vehicle(Stringname)

Now we can create a Car class with a constructor. The Car class is derived from the Vehicle class, so it inherits and can access all the members and methods declared as protected or public in the base class:

public class Car extends Vehicle
  public Car(String name)


In broad terms, polymorphism gives us an option to use the same interface for entities of different types. There are two major types of polymorphism, compile time and runtime. Say you have a Shape class that has two area methods. One returns the area of a circle and it accepts single integer; that is, the radius is input and it returns the area. Another method calculates the area of a rectangle and takes two inputs, length and breadth. The compiler can decide, based on the number of arguments in the call, which area method is to be called. This is the compile-time type of polymorphism.

There is a group of techies who consider only runtime polymorphism as real polymorphism. Runtime polymorphism, also sometimes known as subtyping polymorphism, comes into play when a subclass inherits a superclass and overrides its methods. In this case, the compiler cannot decide whether the subclass implementation or superclass implementation will be finally executed, and hence a decision is taken at runtime.

To elaborate, let's take our previous example and add a new method to the vehicle type to print the type and name of the object:

public String toString()
  return "Vehicle:"+name;

We override the same method in the derived Car class:

public String toString()
  return "Car:"+name;

Now we can see subtyping polymorphism in action. We create one Vehicle object and one Car object. We assign each object to a Vehicle variable type because a Car is also a Vehicle. Then we invoke the toString method for each of the objects. For vehicle1, which is an instance of the Vehicle class, it will invoke the Vehicle.toString() class. For vehicle2, which is an instance of the Car class, the toString method of the Car class will be invoked:

Vehicle vehicle1 = new Vehicle("A Vehicle");
Vehicle vehicle2 = new Car("A Car")

Declarative programming

Let's go back to the real-life imperative example, where we gave directions to a friend on how to get to a place. When we think in terms of the declarative programming paradigm, instead of telling our friend how to get to the specific location, we can simply give him the address and let him figure out how to get there. In this case, we tell him what to do and we don't really care if he uses a map or a GPS, or if he asks somebody for instructions: Be at the junction between Fifth Avenue and Ninth Avenue at 9:30 in the morning.

As opposed to imperative programming, declarative programming is a programming paradigm that specifies what a program should do, without specifying how to do it. Among the purely declarative languages are database query languages, such as SQL and XPath, and regular expressions.

Declarative programming languages are more abstract compared to imperative ones. They don't mimic the hardware structure, and, as a consequence, they don't change the programs' states but transform them to new states, and are closer to mathematical logic.

In general, the programming styles that are not imperative are considered to fall in the declarative category. This is why there are many types of paradigms that fall under the declarative category. In our quest, we will look at the only one that is relevant to the scope of our journey: functional programming.


Functional programming

Functional programming is a sub-paradigm of declarative programming. As opposed to imperative programming, functional programming does not change the internal state of the program.

In imperative programming, the functions can be regarded more as sequences of instructions, routines, or procedures. They not only depend on the state stored in the memory but can also change that state. This way, invoking an imperative function with the same arguments can produce different results depending on the current program's state, and at the same time, the executed function can change the program's variables.

In functional programming terminology, functions are similar to mathematical functions, and the output of a function depends only on its arguments, regardless of the program's state, which, at the same time, remains unaffected by the execution of the function.

Paradoxically, while imperative programming has existed since computers were first created, the basic concepts of functional programming dates back before that. Most functional languages are based on lambda calculus, a formal system of mathematical logic created in the 1930s by mathematician Alonzo Church.

One of the reasons why functional languages become so popular in those days is the fact that they can easily run in parallel environments. This should not be confused with multithreading. The main feature that allows functional languages to run in parallel is the basic principle on which they reside: the functions rely only on the input arguments and not on the program's state. That is, they can be run anywhere, and the results of the multiple parallel executions are then joined and used further.

Working with collections versus working with streams

Everyone working with Java is aware of collections. We use collections in an imperative way: we tell the program how to do what it's supposed to do. Let's take the following example in which we instantiate a collection of 10 integers, from 1 to 10:

List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 10; i++)

Now, we will create another collection in which we will filter in only the odd numbers:

List<Integer> odds = new ArrayList<Integer>();
for (int val : list)
  if (val % 2 == 0)

At the end, we want to print the results:

for (int val : odds)

As you can see, we wrote quite a bit of code to perform three basic operations: to create a collection of numbers, to filter the odd numbers, and then to print the results. Of course, we could do all the operations in only one loop, but what if we could do it without using a loop at all? After all, using a loop means we tell the program how to do its task. From Java 8 onwards, we have been able to use streams to do the same things in a single line of code:

.range(0, 10)
.filter(i -> i % 2 == 0)
.forEach( System.out::print );

Streams are defined in the package, and are used to manage streams of objects on which functional-style operations can be performed. Streams are the functional correspondent of collections, and provide support for map-reduce operations.

We will further discuss streams and functional programming support in Java in later chapters.


An introduction to Unified Modeling Language

Unified Modeling Language (UML) is a modeling language that helps us to represent how the software is structured; how different modules, classes, and objects interact with each other, and what the relations between them are.

UML is frequently used in association with object-oriented design, but it has a much broader scope. However, that is beyond the scope of this book, so, in the next sections, we will highlight the UML features relevant to this book.

In UML, we can define the structure and behavior of a system, and we can visualize the model or parts of it through diagrams. There are two types of diagram:

  • Structure diagrams are used to represent the structure of a system. There are many types of structure diagrams, but we are only interested in class diagrams. object, package, and component diagrams are similar to class diagrams.
  • Behavior diagrams are used to describe the behavior of a system. Interaction diagrams are a subset of behavior diagrams and are used to describe the flow of control and data among different components of a system. Among the behavior diagrams, the sequence diagram is used extensively in object-oriented design.

Class diagrams are the type of diagrams used most in object-oriented design and development stages. They are a type of structure diagram, and are used to illustrate the structure of classes and the relations among them:

Class diagrams are useful for describing how the classes are structured in an application. Most of the time, just looking at the structure can be enough to be able to understand how the classes interact, but sometimes this is not enough. For those cases, we can use behavior and interaction diagrams, of which the sequence diagram is used to describe class and object interaction. Let's use a sequence diagram to show how the Car and Vehicle objects interact in the inheritance and polymorphism example:

Class relations

In object-oriented programming, besides the inheritance relation that represents one of the fundamental concepts, there are a few other class relations that help us to model and develop complex software systems:

  • Generalization and realization
  • Dependency
  • Association, aggregation, and composition


Inheritance is also called an is-a relationship because the class inherited from another class can be used as the superclass.

When a class represents the shared characteristics of more than one class, it is called a generalization; for example, Vehicle is a generalization of Bike, Car, and Truck. Similarly, when a class represents a special instance of a general class, it is called a specialization, so a Car is a specialization of Vehicle, as shown in the following diagram:

In UML terminology, the relation to describe inheritance is called Generalization.


If generalization is the corresponding term in UML for object-oriented inheritance, realization, in UML, represents the implementation of an interface by a class in object-oriented programming.

Let's assume we create an interface called Lockable, which is implemented only by Vehicles that can be locked. In this case, a version of the previous diagram implementing Lockable for the Car class will look like this:


Dependency is one of the most generic types of UML relationship. It is used to define that one class depends in some way or other on another class, while the other class may or may not depend on the first one. A dependent relationship is used to represent relations that do not fall into one of the cases described in the following sections. Dependency is sometimes called Uses-A relationship.

In general, in object-oriented programming languages dependency is used to describe whether one class contains a parameter of the second class in the signature of a method, or whether it creates instances of the second class by passing them to other classes without using them (without invoking its methods):


An association represents the relationship between two entities. There are two types of association, namely composition and aggregation. In general, an association is represented by an arrow, as shown in the following diagram:


An aggregation is a special type of association. If inheritance is considered to be the is-a relationship, aggregation can be considered to be the HAS-A relationship.

Aggregation is used to describe a relation between two or more classes, when one class contains the other from a logical point of view, but instances of the contained class can live independently of the first class, outside of its context, or can be shared among other classes. For example, a Department HAS-A Teacher; additionally, every Teacher must belong to Department, but if a Department ceases to exist, a Teacher can still be active as shown in the following diagram:


As the name suggests, a class is a composition of another one. This is somewhat similar to aggregation, with the difference being that the dependent class ceases to exist when the main class ceases to exist. For example, a House is made up of a Room, but the Room ceases to exist if the House is destroyed, as shown in the following diagram:

In practice, especially in languages such as Java that have garbage collectors, the boundary between composition and aggregation is not so well defined. Objects are not destroyed manually; when they are no longer referenced, they are automatically destroyed by the garbage collector. For this reason, from a coding point of view, we should not really be concerned if we deal with a composition or an aggregation relationship, but it's important if we want to have a well-defined model in UML.


Design patterns and principles

Software development is a process that is not only about writing code, regardless of whether you are working in a large team or on a one-person project. The way an application is structured has a huge impact on how successful a software application is.

When we are talking about a successful software application, we are not only discussing how the application does what it's supposed to do but also how much effort we put into developing it, and if it's easy to test and maintain. If this is not done in a correct manner, the skyrocketing development cost will result in an application that nobody wants.

Software applications are created to meet needs, which are constantly changing and evolving. A successful application should also provide an easy way through which it can be extended to meet the continuously changing expectations.

Luckily, we are not the first to encounter these problems. Some of the problems have already been faced and handled. These common problems can be avoided or solved if a set of object-oriented design principles and patterns are applied while designing and developing software.

The object-oriented design principles are also called SOLID. These principles are a set of rules that can be applied when designing and developing software, in order to create programs that are easy to maintain and develop. They were first introduced by Robert C. Martin, and they are part of the agile software-development process. The SOLID principles include the single responsibility principle, open/closed principle, Liskov Substitution Principle, Interface Segregation Principle, and dependency inversion principle.

In addition to the design principles, there are object-oriented design patterns. Design patterns are general reusable solutions that can be applied to commonly occurring problems. Following Christopher Alexander's concept, design patterns were first applied to programming by Kent Beck and Ward Cunningham, and they were popularized by the so-called Gang Of Four (GOF) book in 1994. In the following section, we will present the SOLID design principles, which will be followed by the design patterns in the next chapters.

Single responsibility principle

The single responsibility principle is an object-oriented design principle that states that a software module should have only one reason to change. In most cases, when writing Java code, we will apply this to classes.

The single responsibility principle can be regarded as a good practice for making encapsulation work at its best. A reason to change is something that triggers the need to change the code. If a class is subject to more than one reason to change, each of them might introduce changes that affect others. When those changes are managed separately but affect the same module, one set of changes might break the functionality related to the other reasons for change.

On the other hand, each responsibility/reason to change will add new dependencies, making the code less robust and harder to change.

In our example, we will use a database to persist the objects. Let's assume that, for the Car class, we will add methods to handle the database operations of create, read, update, and delete, as shown in the following diagram:

In this case, the Car will not only encapsulate the logic, but also the database operations (two responsibilities are two reasons to change). This will make our classes harder to maintain and test, as the code is tightly coupled. The Car class will depend on the database, so if in the future we want to change the database system, we have to change the Car code. This might generate errors in the Car logic.

Conversely, changing the Car logic might generate errors in the data persistence.

The solution would create two classes: one to encapsulate the Car logic and the other to be responsible for persistence:

Open/closed principle

This principle is as follows:

"Modules, classes, and functions should be open for extension but closed for modifications."

Applying this principle will help us to develop complex and robust software. We must imagine the software we develop is building a complex structure. Once we finish a part of it, we should not modify it any more but build on top of it.

When developing software, it's the same. Once we have developed and tested a module, if we want to change it, we must test not only the functionality we are changing but the entire functionality it's responsible for. That involves a lot of additional resources, which might not have been estimated from the beginning, and also can bring additional risks. Changes in one module might affect functionality in others or on the whole. The following is a diagrammatic representation:

For this reason, best practice is to try to keep modules unchanged once finished and to add new functionality by extending them using inheritance and polymorphism. The open/closed principle is one of the most important design principles being the base for most of the design patterns.


Liskov Substitution Principle

Barbara Liskov states that, Derived types must be completely substitutable for their base types. The Liskov Substitution Principle (LSP) is strongly related to subtyping polymorphism. Based on subtyping polymorphism in an object-oriented language, a derived object can be substituted with its parent type. For example, if we have a Car object, it can be used in the code as a Vehicle.

The LSP states that, when designing the modules and classes, we must make sure that the derived types are substitutable from a behavior point of view. When the derived type is substituted with its supertype, the rest of the code will operate with it as it is the subtype. From this point of view, the derived type should behave as its supertype and should not break its behavior. This is called strong behavioral subtyping.

In order to understand the LSP, let's take an example in which the principle is violated. While we are working on the car-service software, we discover we need to model the following scenario. When a car is left for service, the owner leaves the car. The service assistant takes the key and, when the owner leaves, he goes to check that he has the right key and that he has spotted the right car. He simply goes to unlock and lock the car, then he puts the key in a designated place with a note on it so the mechanic can easily pick it up when he has to inspect the car.

We already have defined a Car class. We are now creating a Key class and adding two methods into the car class: lock and unlock. We add a corresponding method, so the assistant checks the key matches the car:

public class Assistant
  void checkKey(Car car, Key key)
    if ( car.lock(key) == false ) System.out.println("Alert! Wrong 
    key, wrong car or car lock is broken!");

The diagram is as follows:

While working on our software, we realize that buggies are sometimes repaired through the car service. As buggies are four-wheel cars, we create a Buggy class, which is inherited from the Car:

Buggies don't have doors, so they cannot be locked or unlocked. We implement our code accordingly:

public bool lock(Key key)
  // this is a buggy so it can not be locked return false;

We design our software to work with cars, regardless of whether they are buggies or not, so in the future we might extend it with other types of cars. A problem may arise from the fact that cars are expected to be locked and unlocked.

Interface Segregation Principle

The following quote is taken from link:

"Clients should not be forced to depend upon interfaces that they don't use."  

When applied, the Interface Segregation Principle (ISP) reduces the code coupling, making the software more robust, and easier to maintain and extend. ISP was first announced by Robert Martin, when he realized that if the principle is broken and clients are forced to depend on interfaces they don't use, the code becomes so tightly coupled that it's almost impossible to add new functionality to it.

In order to better understand this, let's again take the car-service example (refer to the following diagram). Now we need to implement a class named Mechanic. The mechanic repairs cars, so we add a method of repair car. In this case, the Mechanic class depends upon the I class. However, the Car class exposes a richer sets of methods than the Mechanic class needs:

This is a bad design because if we want to replace a car with another one, we need to make changes in the Mechanic class, which violates the open/closed principle. Instead, we must create an interface that exposes only the relevant methods required in the Mechanic class, as shown in the following diagram:

Dependency inversion principle

"High-level modules should not depend on low-level modules. Both should depend on abstractions."

"Abstractions should not depend on details. Details should depend on abstractions."

In order to understand this principle, we must explain the important concept of coupling and decoupling. Coupling refers to the degree to which modules of a software system are dependent on one another. The lower the dependency is, the easier it is to maintain and extend the system.

There are different approaches to decoupling the components of a system. One of them is to separate the high-level logic from the low-level modules, as shown in the following diagram. When doing this, we should try to reduce the dependency between the two by making them depend on abstractions. This way, any of them can be replaced or extended without affecting other modules:



In this chapter, we presented the main programming paradigms used in Java. We have learned that two different paradigms, such as imperative programming and functional programming, can coexist in the same language; and we have learned how Java went from pure, imperative object-oriented programming to integrating functional programming elements.

Although Java introduced new functional elements, starting from version 8, it is at its core still an object-oriented language. In order to write solid and robust code that is easy to extend and maintain, we learned about the fundamental principles of object-oriented programming languages.

An important part of developing software is designing the structure and the desired behavior of the components of our programs. This way, we can work on large systems, in large teams, sharing our object-oriented designs within or between teams. In order to be able to do this, we highlighted the main UML diagrams and concepts relevant to object-oriented design and programming. We also use UML extensively in our book to describe the examples.

After introducing the class relationships and showing how to represent them in diagrams, we dove into the next section, where we described what the object-oriented design patterns and principles are, and we presented the main principles.

In the next chapter, we will move on to presenting the group of design patterns dealing with object creation in such a way that our code is robust and extendable.

About the Authors

  • Kamalmeet Singh

    Kamalmeet Singh got his first taste of programming at the age of 15, and he immediately fell in love with it. After spending over 14 years in the IT Industry, Kamal has matured into an ace developer and a technical architect. He is also the coauthor of a book on Design Patterns and Best Practices in Java. The technologies he works with range from cloud computing, machine learning, augmented reality, serverless applications to microservices and so on.

    Browse publications by this author
  • Adrian Ianculescu

    Adrian Ianculescu is a software developer with 20 years programming experience, of which 12 years were in Java, starting with C++, then working with C#, and moving naturally to Java. Working in teams ranging from 2 to 40, he realized that making software is not only about writing code and became interested in software design and architecture, in different methodologies and frameworks. After living the corporate life for a while, he started to work as a freelancer and entrepreneur following his childhood passion to make games.

    Browse publications by this author
  • Lucian-Paul Torje

    Lucian-Paul Torje is an aspiring software craftsman working in software industry for almost 15 years. He is interested in almost anything that has to do with technology. This is why he has worked with everything from MS-DOS TSR to microservices, from Atmel microcontrollers to Android, iOS and Chromebooks, from C/C++ to Java, and from Oracle to MongoDB. Whenever someone is needed to use new and innovative approaches to solve a problem, he is keen to give it a go!

    Browse publications by this author

Latest Reviews

(4 reviews total)
Great reference work for modern Java/
Would have preferred more in-depth treatment of the topic.
Good written book. Learn new things from the book.
Design Patterns and Best Practices in Java
Unlock this book and the full library for $5 a month*
Start now