LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

This post is also available in the following languages. Japanese

Improving code quality - Session 5: Bad enumerations drive out good layers

Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.

This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.

Bad enumerations drive out good layers

Let's assume that a service has defined the following enumeration for "user account types":

enum class AccountType { FREE, PERSONAL, UNLIMITED }

When reading and writing this value using local storage, databases, or APIs over the network, mechanisms called "converters" or "mappers" are used to convert between language-specific objects and byte sequences defined in interface definition languages or protocols.

For example, in Android application development, you can use a persistence library called Room, which allows easy implementation of converters using a mechanism called TypeConverter.

However, there are issues with the following use of TypeConverter. What might those issues be?

class AccountTypeConverter {
    @TypeConverter
    fun fromStringValue(typeString: String): AccountType = AccountType.valueOf(typeString)

    @TypeConverter
    fun toStringValue(type: AccountType): String = type.name
}
class AccountTypeConverter {
    @TypeConverter
    fun fromIntValue(typeInt: Int): AccountType = AccountType.values()[typeInt]

    @TypeConverter
    fun toIntValue(type: AccountType): Int = type.ordinal
}

Converters as a layer of corruption prevention

The issue with this code is that by using the enumeration properties name and ordinal, the converter fails to act as a layer of corruption prevention.

This issue allows changes in external interfaces or protocols to affect the code using the enumeration, or in reverse. If there are changes in the values used in databases or remote APIs, it would necessitate changes in the enumeration definitions, affecting the code using them. On the other hand, there might be a need to rename or reorder the enumerators for internal reasons. However, changes in names or order directly affect the externally used values, restricting freedom in making such changes.

Using ordinal can cause specific issues. Suppose you want to add a new AccountType called BUSINESS. From a pricing and feature perspective, it makes sense to place BUSINESS between PERSONAL and UNLIMITED.

enum class AccountType { FREE, PERSONAL, BUSINESS, UNLIMITED }

However, this change alters the value of UNLIMITED.ordinal, necessitating updates to the externally used values as well.

Similarly, using name can cause issues. For example, if you want to rebrand and change the names of the enumerators from FREE, PERSONAL, UNLIMITED to BRONZE, SILVER, GOLD, implementing this change directly would break the values already in use externally.

The frightening aspect of using ordinal and name is that even simple refactoring from the perspective of the code using the enumeration can inadvertently introduce bugs. It's not immediately obvious that such changes would affect the externally used values.

Wrapping to prevent corruption

To solve this problem, it's beneficial to separate the declarations of the enumerators from the externally used values. Here's a method to define the externally used values as properties of the enumeration:

enum class AccountType(val dbValue: String) {
    FREE("free"),
    PERSONAL("personal"),
    UNLIMITED("unlimited");

    companion object {
        val DB_VALUE_TO_TYPE_MAP: Map<String, AccountType> =
            values().associateBy(AccountType::dbValue)
    }
}

This approach protects the externally used values from changes in the names or order of the AccountType enumerators.

If it's necessary to hide the dbValue from the code using AccountType, define a separate converter class where you can define functions and properties corresponding to dbValue and DB_VALUE_TO_TYPE_MAP.

Exception: Eating on the spot prevents corruption

If you use the enumeration only as a "temporary conversion" without converting it to externally used values, using name and ordinal is less likely to cause problems. For example, this might apply when saving integers instead of enumerators as an in-memory cache. However, even in such cases, it's better to use the enumerators themselves as much as possible to benefit from type safety.


In summary: In code that converts externally defined values, define internal and external values independently.

Keywords: value conversion, externally defined value, enum