Production code: Top 5 Best practices illustrated with Java

Arvind Telharkar
Effective Java
Published in
5 min readDec 5, 2021

Writing production code is an art which is only mastered through experience. For a codebase being used in production, the practices used can make a big difference in terms of maintainability and overall ease of debugging. Let’s look at a few best practices which I have gathered during my experience in the industry so far. I will be using Java to illustrate examples.

Photo by Alex Chumak on Unsplash

1. Immutability

Making your objects immutable can save you the hassle of managing numerous states that your mutable objects can land up in. Immutable objects have only 1 state and are way more easier to manage in general when it comes to debugging. In Java, the “final” keyword is used for immutability.

For e.g. An immutable Point class would look as follows

// Immutable Point class
public final class Point {
// Immutable final fields which are private
private final int xCoordinate;
private final int yCoordinate;
public Point(final int xCoordinate, final int yCoordinate) {
this.xCoordinate = xCoordinate;
this.xCoordinate = xCoordinate;
}

In the example above, once a Point object is created, its x and y coordinates should not be altered. If they are(i.e. if there is a need), that would be a different point object!

For more on immutability in Java — This article on the “Builder pattern” is a good read.

2. Enums for well-defined values

If you have an entity in your code which can have a specified set of values, all of which are well-defined — enums should be used. You can write a switch-case instead of writing an if-else ladder.

For e.g. Consider a Task object which has 3 states — “Initializing”, “In-Progress “ and “Done”.

The following would be an if-else ladder which is bad for maintaining in the long run and also gives little control over the states that are being handled in our code —

if(task.state == "Initializing") {
// Do Something
} else if(task.state == "In-Progress") {
// Do something different
} else if(task.state == "Done") {
// Do something else
}.....// The ladder can go on extending

In the example above, if the ladder gets sufficiently big with the possible values for a task being scattered all over the codebase, managing this task object’s states can quickly become difficult, leading to bugs — due to several states not being accounted for while making a change.

Here is how this same example can be illustrated using enums —

public enum TaskState {
INITIALIZING,
IN_PROGRESS,
DONE
}

The if else ladder would simply translate to a switch-case in this case

switch(task.state) {case INITIALIZING: // Do something
break;
case IN_PROGRESS: // Do something different
break;
case DONE: // Do something else
break;
default: //invalid state or whatever default behaviour is desired
}

The enum TaskState enables us to capture all the possible states in a single place, giving better control.

3. Logging

Logging is extremely important for applications running in production. Logs tell us about the various events happening in the application’s flow, their sequence, erroneous points in the code and a ton of other things. The following are a few pointers to be aware of with logging-

  • Choose the correct logging level. The various log levels are generally “INFO”, “DEBUG”, “ERROR”, “WARN” etc. DEBUG logging should never be enabled in production by default. It should only be enabled for troubleshooting, whenever there is an issue.
  • Never log sensitive data like passwords or other personal information.
  • Make sure your logs contain timestamps! If you use standard loggers in any language, this should be taken care of, but still it is better to be sure. You want to know WHEN something happened when you are debugging something in the middle of the night.
  • Try to ensure that log messages are in machine-parsable format. This helps in building tools for automation on top of your existing logging, finding error patterns etc.
  • For more on logging — This article by Splunk is a good read.

4. Dependency Injection

This is a pretty standard practice used in most companies and therefore should not come as a surprise. Any class which has dependencies should not have to construct them on its own.

For e.g. A Department class having which instantiates its dependencies(Bad!)

public final class Department {
private final Manager manager;
private final List<Employee> employees;
public Department() {
// Instantiate dependency for Manager object
this.manager = new Manager("Alex");
// Instantiate dependency for Employees
this.employees = new ArrayList<Employee>();
}
public final class Manager {
private final String name;
public Manager(String name) {
this.name = name;
}
}
public final class Employee {....}

Now, let’s assume that in the course of time, the implementation of Manager class changes and it also starts taking in age along with name as a mandatory parameter to construct the Manager object. This would look like

public final class Manager {
private final String name;
private final int age;
public Manager(String name, int age) {
this.name = name;
this.age = age;
}

What does this change in implementation mean? The Department class which is relying on Manager class would also have to change its implementation to account for the age, along with the name. Does the Department class care about how the Manager object was constructed? — No!! Department class only needs a Manager object — ready to be used! It should not have to worry about how Manager is instantiated!

This is addressed with Dependency injection, where a class does not need to instantiate its dependencies.

The same example with constructor dependency injection would look as follows —

public final class Department {
private final Manager manager;
private final List<Employee> employees;
public Department(final Manager manager, final List<Employee> employees) {
/*
We don't care how this was constructed. Just give me
a Manager object- However you want to construct it!
*/
this.manager = manager; // Same argument applies to this dependency too
this.employees = employees;
}

5. Composition over inheritance

While modelling a certain behaviour, it is preferable to use interfaces instead of using abstract classes. I have used abstract classes very rarely — Only when there is a really special hierarchy that requires the use of an abstract class. Abstract classes bring in tight coupling, whereas interfaces keep things loosely coupled. In short, prefer “Has-A” over “Is-A” whenever possible. In the future if the behaviour needs to be modified, you can just change or remove “Has-A”, which means your object no longer has that attribute or dependency. It might not be straightforward to do such a modification with a “Is-A” without changing several things fundamentally. Also, overriding of methods involved in inheritance can make things difficult to maintain long-term. In Java, you can’t inherit more that one abstract class, but you can implement more than one interface!

There are lots more!!..

If you want to connect with me, feel free to reach out on LinkedIn!

To take your Java development skills to the next level, be sure to checkout and follow Effective Java!

--

--