Curly Braces #11: Writing SOLID Java code

July 26, 2023 | 10 minute read
Text Size 100%:

Following the five principles of SOLID object-oriented design will make you a better Java developer.

In this article, I am going to talk about writing SOLID Java code. No, I’m not talking about the excellent, classic book by Steve Maguire, Writing Solid Code, now out in its 20th anniversary second edition. Rather, I’m talking about the SOLID principles of object-oriented design (OOD), as taught by Robert C. Martin.

When my own journey began, I was taught to write a lot of procedural code in assembly language and C. As a computer science student and at my first professional programming job, I wrote a mix of procedural and functional C. The object-oriented programming (OOP) movement was in full swing when I moved from C to C++, and I embraced OOP completely before moving to Java.

Then, I studied the OOP works of Grady Booch and The Unified Modeling Language User Guide by the “three amigos” (Booch, Ivar Jacobson, and James Rumbaugh) as both a software design and development paradigm and as a diagramming standard for OOP. It seemed the entire world was on board with OOP and procedural coding was in the past.

However, a few things happened: the growth of SQL databases, the emergence of the World Wide Web, and the growth of automation. With these came SQL, HTML, and bash, Python, Perl, PHP, JavaScript scripting languages, and others. These were all more functional than imperative—and certainly more functional than object oriented.

Functional programming is based on mathematical concepts, and it relies on clearly defining inputs and outputs, embracing principles such as the reduction of side effects, immutability, and referential transparency. Further, functional programming is often associated with declarative programming, where you describe what you want the computer to do, versus telling the computer exactly how to do it, by structuring your code around real-world objects and mimicking their behavior in an imperative way.

While Java is fundamentally an imperative language, Java has embraced functional programming and the declarative nature that often comes with it.

Just as you can do OOP with C, you can do functional programming in Java. For example, objects help define boundaries and perform grouping that can be useful in any type of programming; this concept works for functional programming as well. And just as objects and inheritance (or composition) allow you to divide and conquer, by building up from smaller units of implementation in OOP, you can build complex algorithms from many smaller functions.

Doing Java a SOLID

Maybe OOP concepts aren’t quite dead yet, and mature dogs such as Java are capable of new functional tricks. But SOLID is more about design than programming. For example, I would suggest that even pure functional programmers think in objects, giving entities and components names that describe what they do as much as what they are. For example, maybe you have a TradingEngine, an Orchestrator, or a UserManager component.

Regardless of the paradigm, everyone wants code to be understandable and maintainable. SOLID, which is made of the following five principles from which its name derives, fosters those very same goals:

  • SRP: The single-responsibility principle
  • OCP: The open-closed principle
  • LSP: The Liskov substitution principle
  • ISP: The interface segregation principle
  • DIP: The dependency inversion principle

Next, I’ll discuss these principles in the context of Java development.

Single-responsibility principle

SRP states that a class should have a single responsibility or purpose. Nothing else should require the class to change outside of this purpose. Take the Java Date class, for example. Notice there are no formatting methods available in that class. Additionally, older methods that came close to formatting, such as toLocaleString, have been deprecated. This is a good example of SRP.

With the Date class, there are concise methods available for creating objects that represent a date and for comparing two different Date objects (and their representative dates in terms of time). To format and display a Date object, you need to use the DateFormat class, which bears the responsibility to format a given Date according to a set of flexible criteria. For example, for this code,

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
Date d = new Date();
String s = DateFormat.getDateInstance().format(d);

the output will be

July 4, 2023

If you need to change how dates are represented within the system, you change only the Date class. If you need to change the way dates are formatted for display, you change just the DateFormat class, not the Date class. Combining that functionality into one class would create a monolithic behemoth that would be susceptible to side effects and related bugs if you changed one area of responsibility within the single codebase. Following the SOLID principle of SRP helps avoid these problems.

Open-closed principle

Once a class is complete and has fulfilled its purpose, there may be a reason to extend that class but you should not modify it. Instead, you should use generalization in some form, such as inheritance or delegation, instead of modifying the source code.

Look at the DateFormat class’s Javadoc, and you’ll see that DateFormat is an abstract base class, effectively enforcing OCP. While DateFormat has methods to specify a time zone, indicate where to insert or append a formatted date into a given StringBuffer, or handle types of calendars, SimpleDateFormat extends DateFormat to add more elaborate pattern-based formatting. Moving from DateFormat to SimpleDateFormat gives you everything you had before—and a whole lot more. For example, for the following code,

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
Date d = new Date();
SimpleDateFormat sdf = 
    new SimpleDateFormat("YYYY-MM-dd HH:MM:SS (zzzz)");
String s = sdf.format(d);

the output will be

2023-07-04 09:07:722 (Eastern Daylight Time)

The important point is that the DateFormat class is left untouched from a source-code perspective, eliminating the chance to adversely affect any dependent code. Instead, by extending the class, you can add new functionality by isolating changes in a new class, while the original class is still available and untouched for both existing and new code to use.

Liskov substitution principle

LSP, developed by Barbara Liskov, states that if code works with a given class, it must continue to work correctly with subclasses of that base class. Although LSP sounds simple, there are all sorts of examples that show how hard it is to enforce and test LSP.

One common example involves shapes with a Shape base class. Rectangle and Square subclasses behave differently enough that substituting subclasses and requesting the area may yield unexpected results.

Using the Java libraries as an example, the Queue family of Java collection classes looks promising for conforming to LSP. Starting with the abstract base class AbstractQueue, along with subclasses ArrayBlockingQueue and DelayQueue, I created the following simple test application, fully expecting it to conform to LSP:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
public class LSPTest {
    static AbstractQueue<MyDataClass> q = 
            new ArrayBlockingQueue(100);
            //new DelayQueue();
    public static void main(String[] args) throws Exception {
        for ( int i = 0; i < 10; i++ ) {
            q.add( getData(i+1) );

        MyDataClass first = q.element();
        System.out.println("First element data: " +first.val3);
        int i = 0;
        for ( MyDataClass data: q ) {
            if ( i++ == 0 ) {
                test(data, first);

            System.out.println("Data element: " + data.val3);
        MyDataClass data = q.peek();
        test(data, first);
        int elements = q.size();
        data = q.remove();
        test(data, first);
        if ( q.size() != elements-1 ) {
            throw new Exception("Failed LSP test!");
        if ( ! q.isEmpty() ) {
            throw new Exception("Failed LSP test!");
    public static MyDataClass getData(int i) {
        Random rand = new Random(i); 
        MyDataClass data = new MyDataClass();
        data.val1 = rand.nextInt(100000);
        data.val2 = rand.nextLong(100000);
        data.val3 = ""+data.val1+data.val2;
        return data;
    public static void test(MyDataClass d1, MyDataClass d2) throws Exception{
        if ( ! d1.val3.equals(d2.val3) ) {
            throw new Exception("Failed LSP test!");

But my code doesn’t pass the LSP test! It fails for two reasons: The behavior of add in the DelayQueue class requires the elements to implement the Delayed interface, and even when that is fixed, the implementation of remove has been, well, removed. Both violate LSP.

I did find, however, that AbstractBlockingQueue and ConcurrentLinkedQueue passed the LSP tests. This is good, but I hoped there would have been more consistency.

Interface segregation principle

With OOP, it’s easy to get carried away. For example, you can create a Document interface and then define interfaces that represent other documents such as text documents, numeric documents (such as a spreadsheet), or presentation-style documents (such as slides). This is fine, but the temptation to add behavior to these already rich interfaces can add too much complexity.

For example, assume the Document interface defines basic methods to create, store, and edit documents. The next evolution of the interface might be to add formatting methods for a document and then to add methods to print a document (effectively telling a Document to “print itself”). On the surface, this makes sense, because all the behavior dealing with documents is associated with the Document interface.

However, when that’s implemented, it means all of the code to create documents, store documents, format documents, print documents, and so on—each an area of substantial complexity—comes together in a single implementation that can have drawbacks when it comes to maintenance, regression testing, and even build times.

ISP breaks these megainterfaces apart, acknowledging that creating a document is a very different implementation from formatting a document and even printing a document. Developing each of those areas of functionality requires its own center of expertise; therefore, each should be its own interface.

As a byproduct, this also means that other developers need not implement interfaces that they never intend to support. For example, you can create a slide deck and then format it to only be shared via email and never printed. Or you might decide to create a simple text-based readme file, as opposed to a richly formatted document used as marketing material.

A good example of ISP in practice in the JDK is the java.awt set of interfaces. Take Shape, for example. The interface is very focused and simple. It contains methods that check for intersection, containment, and support for path iteration of the shape’s points. However, the work of actually iterating the path for a Shape is defined in the PathIterator interface. Further, methods to draw Shape objects are defined in other interfaces and abstract base classes such as Graphics2D.

In this example, java.awt doesn’t tell a Shape to draw itself or to have a color (itself a rich area of implementation); it’s a good working example of ISP.

Dependency inversion principle

It’s natural to write higher-level code that uses lower-level utility code. Maybe you have a helper class to read files or process XML data. That’s fine, but in most cases, such as code that connects to databases, JMS servers, or other lower-level entities, it’s better not to code to them directly.

Instead, DIP says that both lower-level and higher-level constructs in code should never depend directly on one another. It’s best to place abstractions, in the form of more general interfaces, in between them. For example, in the JDK, look at the java.sql.* package or the JMS API and set of classes.

With JDBC, you can code to the Connection interface without knowing exactly which database you’re connecting to, while lower-level utility code, such as DriverManager or DataSource, hides the database-specific details from your application. Combine this with dependency injection frameworks such as Spring or Micronaut, and you can even late-bind and change the database type without changing any code.

In the JMS API, the same paradigm is used to connect to a JMS server, but it goes even further. Your application can use the Destination interface to send and receive messages without knowing whether those messages are delivered via a Topic or a Queue. Lower-level code can be written or configured to choose the message paradigm (Topic or Queue) with associated message delivery details and characteristics hidden, and your application code never needs to change. It’s abstracted by the Destination interface in between.

You should write solid SOLID code

SOLID is a rich and deep topic; there’s a lot more to learn about. You can start small. Find solace knowing that the JDK has embraced many of the OOD principles that make writing code more efficient and productive.

Dig deeper

Eric J. Bruno

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

Javadoc documents are great—and better than ever with @snippets

Mohamed Taman | 17 min read

Next Post

Method handles: A better way to do Java reflection

Ben Evans | 18 min read