Software Development

5 pitfalls to avoid when building Spring applications in Java


In the last few months I’ve had the opportunity to review large amounts of code across multiple Spring-based services and teams. Here are the top issues that I’ve identified related to Spring. Hopefully, this can help you build better Spring applications.

Using DTOs in the Domain Layer

If you are building services using Spring, chances are your service looks something like this:

DTOs belong to the API layer, which is responsible for translating between the domain model and DTO. The advantage of this approach is that we can have several APIs, each with a different set of DTOs, working with the same service layer. It might be obvious to mention, but make sure that your services only work with domain model classes. Having the your services use DTOs completely defeats their purpose.

For simple applications in which you don’t actually need to have two separate models (one for the API and one for the Service layer), then you should have your Controller Layer use your domain model classes, rather than having your Service layer use DTOs

Treating Domain Model classes as DTOs

In contrast to DTOs, domain model classes can have functionality. I keep seeing developers use domain model classes that contain only fields but no logic – all the logic ends up in a Service class. For example:

public class Order {
    ...
    private List items = new ArrayList<>();
  
    public List getItems() {
        return items;
    }
    ...
}

public class OrderService {
   ...
   public static double getOrderTotal(Order order) {
         // compute order total
   }
   ...
}

Putting methods in your domain model will make your code more readable and maintainable.

One of the reasons developers give for not doing this is: We are not doing DDD. We have an anemic domain model. But then it’s even more important to put as much logic as possible in your domain model classes. Your Service classes must already contain complex business logic and processes. The more methods you can move out of these Service classes, the easier they will be to maintain. Going back to our example above, you’ll want to do something like this:

public class Order {
    ...
    private List items = new ArrayList<>();
  
    public List getItems() {
        return items;
    }
    
   public double getOrderTotal() {
         // compute order total
   }

    ...}

Autogenerating the domain model classes

When you start working on a new service, you might be tempted to auto-generate your domain model classes using tools like Swagger. The problem with this approach is that it won’t allow you to write any custom logic in your domain classes, effectively turning them into DTOs (see above). Instead, take the time to write your classes manually. Your future self will thank you for it.

Leaking database-specific functionality into the Service layer.

If you’re using Spring Data (or similar) you probably have Repository classes that are responsible for the persistence of data. The main goal of this Repository layer is to make the Service layer database-agnostic. Anything to do with how your specific database works (queries, indexes, keys) has to be encapsulated within the Repository layer. This might seem obvious, but one still sees code like this:

@Service
public class UserService {
    public List findUsers(String name, String status) {
        return userRepository.find("name = '" + name + "' AND status ='" + status + "'");
    }
}    

public interface UserRepository {
    List find(String filter);    
}

In this example, the UserRepository class is providing a method for clients to filter users according to any condition they want. The problem is that it is also exposing internal implementation details in its public interface: it expects clients that call the find method to construct and send SQL filters. Having our services know about SQL defeats the purpose of having a Repository layer. Any time you see SQL queries (or other database-specific constructs) in your service classes, it’s a sign that there are Repository classes that are not exposing the right interface.

Going back to our last example, here is a better implementation:

@Service
public class UserService {
    public List findUsers(String name, String status) {
        Map filters = new HashMap<>();
        
        filters.put("name", name);
        filters.put("status", status);

        return userRepository.find(filters);
    }
}    

public interface UserRepository {
    List find(Map filters);    
}

In this case, the Repository class is exposing an interface that doesn’t require the client to send SQL statements. There will have to be another class, which will be part of the Repository layer, that will take the filters map and generate the corresponding SQL filters.

Using static methods instead of Spring Beans

Putting simple functionality in static methods is fine. However, for more complex functionality, you’ll want to use Spring Beans. Here are the advantages of using Beans over static methods:

  • Beans can access other Beans and the Spring configuration.
  • Beans can have multiple implementations and be swapped at runtime.
  • Beans are very easy to mock when writing tests.

Even if you don’t need those capabilities right now, the overhead of creating a bean over a static class is very small. There is really no reason not to do it.

Software Development
Want to be a great developer? Start reading Schopenhauer
Software Development
Spring Data Couchbase – Pitfalls to Avoid
Software Development
Are You Agile? 6 Signs Your Team May Not Be