framework driven development - annotation driven development

The Problems of Annotation-Driven Development

In this article we will talk about the problems that arise when we overuse annotations in our applications, something we call “annotation-driven development” or also “framework-driven development”. There are a wide variety of reasons to use frameworks with all of their annotations, although there are also a considerable amount of problems that frameworks could bring to us.

We will try to explain all the complexities that are inherent to the use of framework annotations, but at the same time trying to keep an objective perspective to avoid influencing your decisions.

Frameworks are very useful many times, but as always, everything in excess could be harmful.

Let’s start!

Introduction

In the last decade, the use of frameworks like Spring has been widely spread across the software developers community. The use of this kind of frameworks brings huge benefits for us, as we save plenty of time when we are creating a new project especially.

The question is, do they only bring benefits to our codebase? The big majority of the community thinks that there are no drawbacks most of the times; today we are going to try to explain to you that every decision brings advantages and disadvantages. Even the adoption of hugely popular frameworks like Spring!

What’s Good About Them

Frameworks in general have the benefit that they’ve been widely adopted and utilised in production by thousands of companies nowadays. This means that it’s been throughly tested in production, what gives us a big level of confidence when adopting them into our applications.

Another big advantage is that we can save plenty of time by configuring everything in our application using annotations. For instance, we could have our persistence layer working in minutes using Spring Data, just an interface and a few annotations and we have our persistence layer ready for testing.

Apart from saving us a very precious time, Spring abstracts (specially for less experienced developers) some of the common issues we normally have to deal with when building software. What do we mean by that? For example, Spring integrates Resiliency4j library into one of its modules to allow developers to configure resiliency patterns by just using annotations. This simplifies things for some developers that don’t fully understand how to implement these patterns.

One more benefit is that it’s easier, specially in large organisations, to achieve some consistency across teams when using frameworks.

frameworks - consistency
Photo by Maria Teneva on Unsplash

The ability to be able to use an annotation and then “magically” get everything we need working is quite tempting and satisfactory, mainly due to the time we save and the feeling of achievement we get almost immediately.

Everything sounds great so far, although we’ll have to remind you that “all that glitters is not gold”!

Despite of all the benefits we mentioned earlier, a few questions could come to our minds:

  • Do we really understand what Spring is doing under the covers when we use any of these annotations?
  • Do we fully understand all the possible implications in terms of performance, usage of resources or maintainability when we use them?
  • Assuming a new joiner doesn’t know what these annotations do, how likely is that they will be able to understand our code quickly?
  • If something goes wrong in production, do we have enough clarity and visibility over what this code is doing when is running in production?

If the answer of any of these questions is NO, then we think we could have a big problem sooner or later.

Let’s see why!

The Problems

The Framework Developer Disorder

We’ve met many developers in our sector that can be considered “framework developers”. What do we mean with this? We mean that these developers rely mostly on a given framework to develop anything they have to do in their daily work. They cannot even stop for one minute, think and consider if there’s a simpler and clearer way to achieve the same not using annotations or even a framework. Like in some way their sole power is this particular framework.

It seems that they don’t trust themselves sufficiently to write a simple piece of code in their plain language without relying in their favourite framework. Personally, we think that any overhead added to our codebase should be carefully considered. Not only with frameworks, but even with libraries.

It’s very important to have a good understanding about the internals to be able to anticipate problems with performance or even wrong behaviour.

Not Being Aware of Introducing New Weaknesses

Every framework pulls a ton of dependencies into our application once imported, the more dependencies we have, the more vulnerable our application could be to future or existing vulnerabilities.

This means that for every additional dependency, a potential attacker has an additional opportunity to breach our system, specially if we don’t scan our dependencies periodically for new vulnerabilities.

With the introduction of Spring Boot and their starter packages, at least the scope has been reduced considerably. We can now select what set of dependencies or packages we want to include in our application.

Keep your dependencies to the minimum necessary would be our general advice, this will reduce the chances to be affected by a vulnerability.

Everything Occurs Magically

Everything we’ve seen so far is not the only problem we could find. When we use a framework, most of the times we have no idea what the framework is doing under the covers, we are not even aware of what libraries is it using internally or what language capabilities might be using to achieve that functionality. There’s magic everywhere, it’s impossible to understand or even follow the code if you’re not familiar with both the framework and the codebase!

We’ve even seen developers criticising a piece of code in a code review, but at the same time utilising a framework annotation which internally is compiling that annotation to exactly the same piece of code they’re moaning about! Just because they are not aware of what the compiled code will look like once the annotations are all processed at compilation time.

annotations and frameworks - magic
Photo by Julius Drost on Unsplash
Performance Implications

This can get even worse, some annotations can only be processed at runtime if their retention policy has been defined to RUNTIME. This means that in many cases, our code will have to be executed either through an aspect or a proxy. In many cases this could have performance implications, but not many developers are concerned about this problem. It’s very rare to see a developer double checking what an annotation does internally before adopting it, they only care about the goal but not the means to achieve that goal.

For instance, recently we noticed in one of our projects that some of the developers were using a @Retry annotation that Resiliency4j provides. After asking the team, no one in the team was aware of what that annotation was actually doing internally.

After taking a quick look, it seems that this annotation gets processed at runtime. What will happen in this case is that every class annotated with @Retry will be processed through an aspect called RetryAspect. This could have performance implications if our application gets a high amount of requests per second and we need a low latency.

The actual effect of something like this would have to be tested in your system with performance tests or even a microbenchmark, this will tell you if it’ll affect your system’s performance in an acceptable manner or not.

In cases like this, we highly recommend using the library but bootstrap things programatically. In this way everything is in one place and we can easily read and understand how it works; no need for aspects or any complicated solutions.

Unnecessary Annotations

Even in those cases where annotations are processed at compilation time, if we can achieve exactly the same thing with a similar amount of code, there’s no point in using annotations together with their own processors. This will probably slow down your compilation time, considering that we compile multiple times in a day, it’s probably better to avoid them whenever possible.

We know that using annotations is “cool”, but we are not paid to be cool. You should keep something always in mind, every decision always has consequences, some are bad and some are good. The key is to find the solution with less drawbacks that can fulfil your requirements, but also only use what you really need.

An example related to the use of annotations processed at compilation time could be the use of @Mock annotations in Mockito. What’s the real benefit of using them? We can initialise a mock in one line and inject all of our mocks into our implementation’s constructor. In this way everything is explicit in the code, therefore it’s easier to read and, on top of that, I bet your unit tests will run faster.

Additionally, we are forced to use a MockitoExtension for our unit tests. All this overhead will add up when we run our set of unit tests, you don’t believe us? Let’s look at an example.

We are going to include a unit test example using MockitoExtension and @Mock annotation first. This test will just simply return a country code from an existing IP in a client API.

@ExtendWith(MockitoExtension.class)
class GeoLocalisationClientTest {

    @Mock
    private RestTemplate restTemplate;
    @Mock
    private APIConfiguration apiConfig;
    @Mock
    private RequestIdProvider requestIdProvider;
    @Mock
    private MetricsRecorder metricsRecorder;

    @InjectMocks
    private GeoLocalisationClient client;

    @Test
    void shouldReturnAValidCountryCode() {
        
        CountryData countryData = new CountryData("nl");
        Location location = new Location(countryData);
        GeoLocalisationResponse response = new GeoLocalistionResponse(location);
        IpInfo ipInfo = new IpInfo(location);

        when(apiConfig.getIpInfoPath()).thenReturn("ipinfo/{ipAddress}");
        when(restTemplate.exchange(eq("ipinfo/{ipAddress}"),
                eq(GET),
                any(),
                eq(GeoLocalisationResponse.class),
                eq("1.2.3.4")))
                .thenReturn(ResponseEntity.ok(response));

        String countryCode = client.getCountryCode("1.2.3.4");

        assertThat(countryCode).isEqualTo("NL");
    }
}

As you can see, we initialise all our mocks using @Mock annotation and then use @InjectMocks to instantiate our client and inject the previously defined mocks.

If we run this unit test in our IDE, every individual execution takes approximately 350-420ms.

Let’s try now removing those annotations and see how our test would look like.

class GeoLocalisationClientTest {

    private RestTemplate restTemplate = mock(RestTemplate.class);
    private APIConfiguration apiConfig = mock(APIConfiguration.class);
    private RequestIdProvider requestIdProvider = mock(RequestIdProvider.class);

    private GeoLocalisationClient client = new GeoLocalisationClient(restTemplate, apiConfig, requestIdProvider);

    @Test
    void shouldReturnAValidCountryCode() {

        CountryData countryData = new CountryData("nl");
        Location location = new Location(countryData);
        GeoLocalisationResponse response = new GeoLocalistionResponse(location);
        IpInfo ipInfo = new IpInfo(location);

        when(apiConfig.getIpInfoPath()).thenReturn("ipinfo/{ipAddress}");
        when(restTemplate.exchange(eq("ipinfo/{ipAddress}"),
                eq(GET),
                any(),
                eq(GeoLocalisationResponse.class),
                eq("1.2.3.4")))
                .thenReturn(ResponseEntity.ok(response));

        String countryCode = client.getCountryCode("1.2.3.4");

        assertThat(countryCode).isEqualTo("NL");
    }
}

Do you think that our test looks more complex, longer or more difficult to read? We don’t think so. So what’s the real benefit of these annotations then?

What will be shocking for many of you is the time that the same test will take. If we run this same test without the overhead of annotations and the test runner, we can see how our test runs approximately in 35-45ms! This is a huge improvement.

You can see how the processing of annotations can affect our productivity in the best of the scenarios, and in some cases it could even affect the performance of our applications. So please, use them only when it’s really needed; think before taking any decision affecting your codebase and don’t do things just because others do the same.

The problems for our example don’t stop here though. This example is from a real unit test in one of the projects we’ve worked on in the recent months. You will notice that a mock has been instantiated for a MetricsRecorder class in our first test, the problem is that this dependency does not even exist in our component! Why is it there then?

annotations - mockito tests
Photo by Edwin Andrade on Unsplash

One of the problems with using @InjectMocks is that having additional unused dependencies won’t matter. What probably happened in this case is that the dependency was removed from our component, but it couldn’t be noticed because of the use of mock annotations.

This would’ve been flagged instantly if we were using a plain constructor for our test, as the dependency would’ve been removed from our component’s constructor and it wouldn’t even compile then!

So not only these annotations were unnecessary and are not adding any real value, they’re also creating some problems for us.

Lack of readability

Another problem with the use of frameworks and annotations is that following the flow of the code could become difficult. The use of annotations forces us to be jumping from one place to the other and do searches in our codebase to try to understand where everything is being configured.

When we configure and instantiate everything programatically, we’ll never have that problem. The flow could be followed very easily, even by someone not familiar with our codebase.

Debugging becomes difficult

One more issue with frameworks and annotations is that debugging becomes difficult, we’d have to dig into the frameworks code when something goes wrong and make an extra effort to understand and debug that code in order to fix our problem.

This means that fixing problems becomes more expensive, as developers spend more time investigating issues. This is something of huge importance for any organisation.

Conclusion

In this article we’ve seen some of the benefits and the problems of using (or overusing) framework annotations. We’ve tried to keep an objective perspective of what benefits do frameworks bring to us, but at the same time we must be honest with all the problems we’ve encountered with them throughout our careers.

To clarify something before we end, we’re not saying that you shouldn’t use frameworks or annotations. What we mean is that you should use them carefully, and only apply them when they’re providing real value and they’re not bringing more problems than solutions!

If you are interested in reading more of our Java articles you can find them here.

That’s all from us today! We’ve enjoyed writing this article and we hope you’ve also enjoyed reading it as much as we did writing it!

Please follow us for more articles and we hope to see you back soon!

Thanks for reading us!