Back-end13 minute read

Top 10 Most Common Spring Framework Mistakes

Java’s open source Spring framework is a popular tool for creating high performing applications using plain old Java objects, but as with any tool, inappropriate use can lead to trouble. In this article, we cover the most common pitfalls of using the Spring framework so new and experienced developers alike have a roadmap of what to avoid.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Java’s open source Spring framework is a popular tool for creating high performing applications using plain old Java objects, but as with any tool, inappropriate use can lead to trouble. In this article, we cover the most common pitfalls of using the Spring framework so new and experienced developers alike have a roadmap of what to avoid.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Toni Kukurin
Verified Expert in Engineering

Toni enjoys architecting software solutions and applying his engineering skills to solve interesting real-world problems.

Expertise

PREVIOUSLY AT

Google
Share

Spring is arguably one of the most popular Java frameworks, and also a mighty beast to tame. While its basic concepts are fairly easy to grasp, becoming a strong Spring developer requires some time and effort.

In this article we’ll cover some of the more common mistakes in Spring, specifically oriented towards web applications and Spring Boot. As Spring Boot’s website states, Spring Boot takes an opinionated view on how production-ready applications should be built, so this article will try to mimic that view and provide an overview of some tips which will incorporate well into standard Spring Boot web application development.

In case you are not very familiar with Spring Boot but would still like to try out some of the things mentioned, I’ve created a GitHub repository accompanying this article. If you feel lost at any point during the article, I’d recommend cloning the repository and playing around with the code on your local machine.

Common Mistake #1: Going Too Low Level

We’re hitting it off with this common mistake because the “not invented here” syndrome is quite common in the software development world. Symptoms including regularly rewriting pieces of commonly used code and a lot of developers seems to suffer from it.

While understanding the internals of a particular library and its implementation is for the most part good and necessary (and can be a great learning process as well), it’s detrimental to your development as a software engineer to be constantly tackling the same low-level implementation details. There is a reason why abstractions and frameworks such as Spring exist, which is precisely to separate you from repetitive manual work and allow you to concentrate on higher level details– your domain objects and business logic.

So embrace the abstractions - the next time you are faced with a particular problem, do a quick search first and determine whether a library solving that problem is already integrated into Spring; nowadays, chances are you’ll find a suitable existing solution. As an example of a useful library, I’ll be using Project Lombok annotations in examples for the remainder of this article. Lombok is used as a boilerplate code generator and the lazy developer within you hopefully shouldn’t have a problem acquainting yourself with the library. As an example, check out what a “standard Java bean” looks like with Lombok:

@Getter
@Setter
@NoArgsConstructor
public class Bean implements Serializable {
    int firstBeanProperty;
    String secondBeanProperty;
}

As you might imagine, the above code compiles to:

public class Bean implements Serializable {
    private int firstBeanProperty;
    private String secondBeanProperty;

    public int getFirstBeanProperty() {
        return this.firstBeanProperty;
    }

    public String getSecondBeanProperty() {
        return this.secondBeanProperty;
    }

    public void setFirstBeanProperty(int firstBeanProperty) {
        this.firstBeanProperty = firstBeanProperty;
    }

    public void setSecondBeanProperty(String secondBeanProperty) {
        this.secondBeanProperty = secondBeanProperty;
    }

    public Bean() {
    }
}

Take note, however, that you’re most likely going to have to install a plugin in case you intend to use Lombok with your IDE. IntelliJ IDEA’s version of the plugin can be found here.

Common Mistake #2: ‘Leaking’ Internals

Exposing your internal structure is never a good idea because it creates inflexibility in service design and consequently promotes bad coding practices. ‘Leaking’ internals are manifested by making database structure accessible from certain API endpoints. As an example, let’s say the following POJO (“Plain Old Java Object”) represents a table in your database:

@Entity
@NoArgsConstructor
@Getter
public class TopTalentEntity {

    @Id
    @GeneratedValue
    private Integer id;

    @Column
    private String name;

    public TopTalentEntity(String name) {
        this.name = name;
    }

}

Let’s say an endpoint exists which needs to access the TopTalentEntity data. Tempting as it may be to return TopTalentEntity instances, a more flexible solution would be creating a new class to represent the TopTalentEntity data on the API endpoint:

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class TopTalentData {
    private String name;
}

That way, making changes to your database back-end will not require any additional changes in the service layer. Consider what would happen in the case of adding a ‘password’ field to TopTalentEntity for storing your users’ password hashes in the database - without a connector such as TopTalentData, forgetting to change the service front-end would accidentally expose some very undesirable secret information!

Common Mistake #3: Lacking Separation of Concerns

As your application grows, code organization increasingly starts becoming an ever more important matter. Ironically, most of the good software engineering principles start to break down at scale – especially in cases where not much thought has been given to the application architecture design. One of the most common mistakes developers then tend to succumb to is mixing code concerns, and it’s extremely easy to do!

What usually breaks separation of concerns is just ‘dumping’ new functionality into existing classes. This is, of course, a great short-term solution (for starters, it requires less typing) but it inevitably becomes a problem further down the road, be it during testing, maintenance, or somewhere in between. Consider the following controller, which returns TopTalentData from its repository:

@RestController
public class TopTalentController {

    private final TopTalentRepository topTalentRepository;

    @RequestMapping("/toptal/get")
    public List<TopTalentData> getTopTalent() {
        return topTalentRepository.findAll()
                .stream()
                .map(this::entityToData)
                .collect(Collectors.toList());
    }

    private TopTalentData entityToData(TopTalentEntity topTalentEntity) {
        return new TopTalentData(topTalentEntity.getName());
    }

}

At first, it might not seem there’s anything particularly wrong with this piece of code; it provides a list of TopTalentData which is being retrieved from TopTalentEntity instances. Taking a closer look, however, we can see that there are actually a few things that TopTalentController is performing here; namely, it is mapping requests to a particular endpoint, retrieving data from a repository, and converting entities received from TopTalentRepository into a different format. A ‘cleaner’ solution would be separating those concerns into their own classes. It might look something like this:

@RestController
@RequestMapping("/toptal")
@AllArgsConstructor
public class TopTalentController {

    private final TopTalentService topTalentService;

    @RequestMapping("/get")
    public List<TopTalentData> getTopTalent() {
        return topTalentService.getTopTalent();
    }
}

@AllArgsConstructor
@Service
public class TopTalentService {

    private final TopTalentRepository topTalentRepository;
    private final TopTalentEntityConverter topTalentEntityConverter;

    public List<TopTalentData> getTopTalent() {
        return topTalentRepository.findAll()
                .stream()
                .map(topTalentEntityConverter::toResponse)
                .collect(Collectors.toList());
    }
}

@Component
public class TopTalentEntityConverter {
    public TopTalentData toResponse(TopTalentEntity topTalentEntity) {
        return new TopTalentData(topTalentEntity.getName());
    }
}

An additional advantage to this hierarchy is that it allow us to determine where functionality resides just by inspecting the class name. Furthermore, during testing we can easily substitute any of the classes with a mock implementation if the need arises.

Common Mistake #4: Inconsistency and Poor Error Handling

The topic of consistency is not necessarily exclusive to Spring (or Java, for that matter), but still is an important facet to consider when working on Spring projects. While coding style can be up for debate (and is usually a matter of agreement within a team or within an entire company), having a common standard turns out to be a great productivity aid. This is especially true with multi-person teams; consistency allows hand-off to occur without many resources being spent on hand-holding or providing lengthy explanations regarding the responsibilities of different classes

Consider a Spring project with its various configuration files, services and controllers. Being semantically consistent in naming them creates an easily searchable structure where any new developer can manage his way around the code; appending Config suffixes to your configuration classes, Service suffixes to your services and Controller suffixes to your controllers, for example.

Closely related to the topic of consistency, error handling on the server-side deserves a specific emphasis. If you ever had to handle exception responses from a poorly written API, you probably know why– it can be a pain to properly parse exceptions, and even more painful to determine the reason for why those exceptions occurred in the first place.

As an API developer, you’d ideally want to cover all user-facing endpoints and translate them into a common error format. This usually means having a generic error code and description rather than the cop-out solution of a) returning a “500 Internal Server Error” message, or b) just dumping the stack trace to the user (which should actually be avoided at all costs since it exposes your internals in addition to being difficult to handle client-side).

An example of a common error response format might be:

@Value
public class ErrorResponse {

    private Integer errorCode;
    private String errorMessage;

}

Something similar to this is commonly encountered in most popular APIs, and tends to work well since it can be easily and systematically documented. Translating exceptions into this format can be done by providing the @ExceptionHandler annotation to a method (an example of an annotation is in Common Mistake #6).

Common Mistake #5: Improperly Dealing with Multithreading

Regardless of whether it is encountered in desktop or web apps, Spring or no Spring, multithreading can be a tough nut to crack. Problems caused by parallel execution of programs are nerve-wrackingly elusive and often times extremely difficult to debug - in fact, due to the nature of the problem, once you realise you’re dealing with a parallel-execution issue you’re probably going to have to forego the debugger entirely and inspect your code “by hand” until you find the root error cause. Unfortunately, a cookie-cutter solution does not exists for solving such issues; depending on your specific case, you’re going to have to assess the situation and then attack the problem from the angle you deem is best.

Ideally you would, of course, want to avoid multithreading bugs altogether. Again, a one-size-fits-all approach does not exist for doing so, but here are some practical considerations for debugging and preventing multithreading errors:

Avoid Global State

First, always remember the “global state” issue. If you’re creating a multithreaded application, absolutely anything that is globally modifiable should be closely monitored and, if possible, removed altogether. If there is a reason why the global variable must remain modifiable, carefully employ synchronization and track your application’s performance to confirm that it’s not sluggish due to the newly introduced waiting periods.

Avoid Mutability

This one comes straight from functional programming and, adapted to OOP, states that class mutability and changing state should be avoided. This, in short, means foregoing setter methods and having private final fields on all your model classes. The only time their values are mutated is during construction. This way you can be certain that no contention problems arise and that accessing object properties will provide the correct values at all times.

Log Crucial Data

Assess where your application might cause trouble and preemptively log all crucial data. If an error occurs, you will be grateful to have information stating which requests were received and have better insight into why your application misbehaved. It’s again necessary to note that logging introduces additional file I/O and should therefore not be abused as it can severely impact your application’s performance.

Reuse Existing Implementations

Whenever you are in need of spawning your own threads (e.g. for making async requests to different services), reuse existing safe implementations rather than create your own solutions. This will, for the most part, mean utilizing ExecutorServices and Java 8’s neat functional-style CompletableFutures for thread creation. Spring also allows asynchronous request processing via the DeferredResult class.

Common Mistake #6: Not Employing Annotation-Based Validation

Let’s imagine our TopTalent service from earlier requires an endpoint for adding new Top Talents. Furthermore, let’s say that, for some really valid reason, every new name needs to be exactly 10 characters long. One way for going about doing this might be the following:

@RequestMapping("/put")
public void addTopTalent(@RequestBody TopTalentData topTalentData) {
    boolean nameNonExistentOrHasInvalidLength =
            Optional.ofNullable(topTalentData)
         .map(TopTalentData::getName)
   .map(name -> name.length() == 10)
   .orElse(true);

    if (nameNonExistentOrInvalidLength) {
        // throw some exception
    }

    topTalentService.addTopTalent(topTalentData);
}

However, the above (in addition to being poorly constructed) is not really a ‘clean’ solution. We are checking for more than one type of validity (namely, that TopTalentData is not null, and that TopTalentData.name is not null, and that TopTalentData.name is 10 characters long), as well as throwing an exception if the data is invalid.

This can be executed much more cleanly by employing Hibernate validator with Spring. Let’s first refactor the addTopTalent method to support validation:

@RequestMapping("/put")
public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) {
    topTalentService.addTopTalent(topTalentData);
}

@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) {
    // handle validation exception
}

Additionally, we’re going to have to indicate what property we want to validate in the TopTalentData class:

public class TopTalentData {
    @Length(min = 10, max = 10)
    @NotNull
    private String name;
}

Now Spring will intercept the request and validate it before the method is invoked – there is no need to employ additional manual tests.

Another way we could’ve achieved the same thing is by creating our own annotations. Although you will usually only employ custom annotations when your needs exceed Hibernate’s built-in constraint set, for this example let’s pretend that @Length does not exist. You would make a validator which checks for string length by creating two additional classes, one for validating and another for annotating properties:

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { MyAnnotationValidator.class })
public @interface MyAnnotation {

    String message() default "String length does not match expected";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    int value();

}

@Component
public class MyAnnotationValidator implements ConstraintValidator<MyAnnotation, String> {

    private int expectedLength;

    @Override
    public void initialize(MyAnnotation myAnnotation) {
        this.expectedLength = myAnnotation.value();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        return s == null || s.length() == this.expectedLength;
    }
}

Note that in these cases, best practices on separation of concerns require you to mark a property as valid if it is null (s == null within the isValid method), and then use a @NotNull annotation if that is an additional requirement for the property:

public class TopTalentData {
    @MyAnnotation(value = 10)
    @NotNull
    private String name;
}

Common Mistake #7: (Still) Using An XML-Based Configuration

While XML was a necessity for previous versions of Spring, nowadays most of the configuration can be done exclusively via Java code / annotations; XML configurations just pose as additional and unnecessary boilerplate code.

This article (as well as its accompanying GitHub repository) uses annotations for configuring Spring and Spring knows which beans it should wire because the root package has been annotated with a @SpringBootApplication composite annotation, like so:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

The composite annotation (you can learn more about it in the Spring documentation simply gives Spring a hint on which packages should be scanned to retrieve beans. In our concrete case, this means the following under the top (co.kukurin) package will be used for wiring:

  • @Component (TopTalentConverter, MyAnnotationValidator)
  • @RestController (TopTalentController)
  • @Repository (TopTalentRepository)
  • @Service (TopTalentService) classes

If we had any additional @Configuration annotated classes they would also be checked for Java-based configuration.

Common Mistake #8: Forgetting About Profiles

A problem often encountered in server development is distinguishing between different configuration types, usually your production and development configurations. Instead of manually replacing various configuration entries every time you are switching from testing to deploying your application, a more efficient way would be to employ profiles.

Consider the case where you are using an in-memory database for local development, with a MySQL database in production. This would, in essence, mean that you will be using a different URL and (hopefully) different credentials for accessing each of the two. Let’s see how this could be done two different configuration files:

application.yaml file

# set default profile to 'dev'
spring.profiles.active: dev

# production database details
spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal'
spring.datasource.username: root
spring.datasource.password:

application-dev.yaml file

spring.datasource.url: 'jdbc:h2:mem:'
spring.datasource.platform: h2

Presumably you would not want to accidentally perform any actions on your production database while tinkering with the code, so it makes sense to set the default profile to dev. On the server, you can then manually override the configuration profile by providing a -Dspring.profiles.active=prod parameter to the JVM. Alternatively, you can also set your OS’s environment variable to the desired default profile.

Common Mistake #9: Failing to Embrace Dependency Injection

Properly using dependency injection with Spring means allowing it to wire all your objects together by scanning all desired configuration classes; this proves to be useful for decoupling relationships and also makes testing a whole lot easier. Instead of tight coupling classes by doing something like this:

public class TopTalentController {

    private final TopTalentService topTalentService;

    public TopTalentController() {
        this.topTalentService = new TopTalentService();
    }
}

We’re allowing Spring to do the wiring for us:

public class TopTalentController {

    private final TopTalentService topTalentService;

    public TopTalentController(TopTalentService topTalentService) {
        this.topTalentService = topTalentService;
    }
}

Misko Hevery’s Google talk explains the ‘whys’ of dependency injection in depth, so let’s instead see how it’s used in practice. In the section on separation of concerns (Common Mistakes #3), we created a service and controller class. Let’s say we want to test the controller under the assumption that TopTalentService behaves correctly. We can insert a mock object in place of the actual service implementation by providing a separate configuration class:

@Configuration
public class SampleUnitTestConfig {
    @Bean
    public TopTalentService topTalentService() {
        TopTalentService topTalentService = Mockito.mock(TopTalentService.class);
        Mockito.when(topTalentService.getTopTalent()).thenReturn(
                Stream.of("Mary", "Joel").map(TopTalentData::new).collect(Collectors.toList()));
        return topTalentService;
    }
}

Then we can inject the mock object by telling Spring to use SampleUnitTestConfig as its configuration supplier:

@ContextConfiguration(classes = { SampleUnitTestConfig.class })

This then allows us to use context configuration to inject the custom bean into a unit test.

Common Mistake #10: Lack of Testing, or Improper Testing

Even though the idea of unit-testing has been with us for a long time now, a lot of developers seem to either “forget” to do this (especially if it is not required), or simply add it in as an afterthought. This is obviously not desirable since tests should not only verify the correctness of your code, but also serve as documentation on how the application should behave in different situations.

When testing web services, you are rarely doing exclusively ‘pure’ unit-tests, since communication over HTTP usually requires you to invoke Spring’s DispatcherServlet and see what happens when an actual HttpServletRequest is received (making it an integration test, dealing with validation, serialization, etc). REST Assured, a Java DSL for easy testing of REST services, on top of MockMVC, has proven to give a very elegant solution. Consider the following code snippet with dependency injection:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {
        Application.class,
        SampleUnitTestConfig.class
})
public class RestAssuredTestDemonstration {

    @Autowired
    private TopTalentController topTalentController;

    @Test
    public void shouldGetMaryAndJoel() throws Exception {
        // given
        MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given()
                .standaloneSetup(topTalentController);

        // when
        MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get");

        // then
        response.then().statusCode(200);
        response.then().body("name", hasItems("Mary", "Joel"));
    }

}

SampleUnitTestConfig wires a mock implementation of TopTalentService into TopTalentController while all other classes are wired using the standard configuration inferred from scanning packages rooted in Application class’ package. RestAssuredMockMvc is simply used to set up a lightweight environment and send a GET request to the /toptal/get endpoint.

Becoming a Spring Master

Spring is a powerful framework that’s easy to get started with but requires some dedication and time to achieve full mastery. Taking the time to familiarize yourself with the framework will definitely improve your productivity in the long run and ultimately help you write cleaner code and become a better developer.

If you’re looking for further resources, Spring In Action is a good hands-on book covering many core Spring topics.

Hire a Toptal expert on this topic.
Hire Now
Toni Kukurin

Toni Kukurin

Verified Expert in Engineering

Poreč, Croatia

Member since November 17, 2016

About the author

Toni enjoys architecting software solutions and applying his engineering skills to solve interesting real-world problems.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

PREVIOUSLY AT

Google

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

Join the Toptal® community.