SpringBoot: Fall In Love with Enum Mapping

George Berar
6 min readOct 12, 2021

--

Hello and welcome to my first post on Medium since I joined this beautiful community last year in November. I tried to keep it as short as possible but interesting at the same time and I hope you will enjoy it. So let’s dive straight into it!

Before we start you can find the entire code here.

Background

Recently I had the opportunity to work on a complex project in the automotive area which integrated 7 different external services. We had a base entity named Clearance which allowed a Car Insurance company to request data for a vehicle belonging to a Customer. Among other attributes we had a status and as you can imagine the data type was an enum with our own internal values. Something like this:

Clearance statuses

Nothing complicated until now, right?

The Problem

The value of status attribute could change when specific events were ingested from 2 out of the 7 services. Each of these events had their own specific statuses which needed to be mapped to our internal status. For simplicity let’s consider these statuses looked like this:

Service A — specific statuses
Service B — specific statuses

The Solution(s)

1. The Simplest Solution

Based on the KISS (keep it stupid simple) principle the simplest solution is to use if/else or switch blocks:

Solution using switch block

It works and it’s pretty simple to understand, right? But here’s the catch: you need to duplicate the switch block for every external service which needs mapping and whenever new internal or external statuses are added/removed you need to make sure to update these switch statements accordingly. Add the fact that you need to remember the place where these statements reside in the codebase and maybe new external services with other statuses are being integrated (which in fact happened to our project also).

The conclusion is that over time will become a little hard to maintain and all these mappings will be spread across your codebase.

2. The Cleaner Solution

A better solution is to centralize the mappings as static data inside the target enum class, in our case ClearanceStatus, and provide utility methods for getting the correct internal status based on a given input which can be one of the two external statuses. Something like this:

Solution using centralized static mappings and utility methods

It looks cleaner, isn’t it? It does and it solves our problem also, but introduced a hidden one: class pollution. If there was the case to handle another external status from a different service we would need to duplicate the static mapping and utility method to deal with the new status also. This leads to an enum class which will grow over time and which will become a little repetitive and messy at some point. But that’s fine since we don’t expect to have 10 different statuses which need to be mapped to our internal one so I guess we can do a compromise. Except I don’t want to! So I tried to come up with a better, cleaner and more maintainable solution than this.

The result: Dynamic Enum Discovery and Registration or DEDR as I like to call it.

DEDR

The idea behind is that I wanted a way for my enum classes, which require these kind of mappings, to do only one thing: declare the static mappings. And then a custom global mapping provider to ‘scan’ for these mappings, extract and save them into a data structure and offer generic utility methods for searching.

This way I restrict the scope of the enums, which no longer get polluted with utility methods and static blocks, and I have a global mapping provider where I centralize everything, remove duplication and offer generic behavior. The mapping provider can be then used everywhere we need in our codebase by simply injecting it using Spring (e.g. @Autowired annotation as we know it).

Sounds complicated, right? Is not and requires only 4 steps actually.

Step 1. Contract

In this step we define a contract (a functional interface basically) which needs to be implemented by every enum class declaring mappings.

Enum contract defined as a functional interface

Note: the generic type T refers to enum classes only

Step 2. Potential Candidate

Because we want to include in our scan/discovery process only the target enums which actually declare mappings we need a way to mark them as ‘potential candidates’. The solution is to create a custom annotation:

Custom annotation to mark potential candidates

Note: this annotation is designed to be used at class level only and it will not work on method or field for example

Step 3. Enhance Enum

After we defined the contract and our custom annotation is time to enhance our enum class:

Enhanced Clearance status enum

Using the registerMappings method from contract the ClearanceStatus enum is declaring its own specific mappings for ServiceAStatus and ServiceBStatus.

Notice for REJECTED status we mapped two different statuses! This is an additional enhancement that I wanted to introduce in order to deal with multiple statuses which need to be mapped to the same status.

Step 4. Mapping Provider

The last step and the most ‘complicated’ one is to build our global mapping provider which will need to scan and discover the potential candidates but also to register somehow all the mappings into a central place, a data structure, which can be queried easily in a generic way.

The enum mapping provider

The method annotated with @PostConstruct is the most important one because inside we are scanning/discovering the potential candidates and registering the mappings into a data structure, in our case a Set to avoid duplicates. As you already know the method is invoked at runtime after EnumMappingProvider bean was created.

Spring provides an out-of-the-box class which can be used for scanning project packages and discover different components/classes/beans. This class is called ClassPathScanningCandidateComponentProvider and as you can see I’m using it together with a filter to scan only the classes which are annotated with our custom @EnableEnumMappingScan annotation. The package com.example.mappings is given as an example of a base package here under which our potential candidates need to be found.

If potential candidates (enum classes annotated with our custom annotation) are found then I’m doing an additional check to make sure that indeed each candidate is implementing the contract and also provides some static mappings. If the condition is not fulfilled then I exclude that enum class from registration process. However, if the condition is met then using reflection I’m invoking the registerMappings method from the candidate and save the result into the global mappings Set.

4.1 Utility methods

I implemented two utility methods because I wanted to have a bidirectional association between statuses (from external to internal and vice versa).

The first method called mappingFromSourceToTarget is used to get an external status based on the given internal status while the mappingToTargetFromSource is used to get an internal status based on the given external one. You can see below some examples which involve the ClearanceStatus and ServiceAStatus but it works the same for ServiceBStatus and for any other statuses which will be mapped:

Examples of using the utility methods

Conclusion

The DEDR started as a simple POC from my own initiative because I was tired of maintaining our enum mappings and seeing the same behavior over and over again. In the end after many team discussions and agreements it was adopted as a full solution which had a positive impact on our development lifecycle because our enums become cleaner and more maintainable than before and offered us a flexible and simple way to control different status mappings by looking in one single place, the target enum.

Please keep in mind this approach might or might not suit your project context or needs and I’m not in the position to say there’s no other way to do it differently or better. I really hope you enjoyed it and had fun reading it.

Stay safe and remember you can find the code here!

--

--

George Berar

Senior Software Engineer • Freelancer • Tech Enthusiast