On enums, annotations and domain modeling

cyclone fence in shallow photography

In this post I’d like to review a case where the use of the very common @JsonProperty annotation leads to unintended coupling.

Let’s take a flight reservation system as our domain example. Each passenger can select his meal preference. We represent this preference with an enum:

enum class MealPreference {
  NONE,
  VEGETARIAN,
  VEGAN,
  LACTO_OVO,
  KOSHER,
  INDIAN
}

This enum is part of the Passenger record:

data class Passenger(
    val firstName: String,
    val lastName: String,
    val emailAddress: String,
    val mealPreference: MealPreference
)

Our app would like to let the passenger edit his profile and change his meal preference. There are some considerations we need to take into account:

  • The list of preferences could change – we want to maintain the list in a central place so that we don’t need to update separate frontend and backend code. The app can query the backend for a list of valid drop-down values.
  • We would obviously like to show human readable text in our app, not uppercase constants with underscores

One really simple approach is to add the @JsonProperty annotation:

enum class MealPreference {
  @JsonProperty("None")
  NONE,

  @JsonProperty("Vegetarian")
  VEGETARIAN,

  @JsonProperty("Vegan")
  VEGAN,

  @JsonProperty("Lacto/Ovo")
  LACTO_OVO,

  @JsonProperty("Kosher")
  KOSHER,

  @JsonProperty("Indian")
  INDIAN;

  val allValues
    get() = values()
}

This looks like a great solution! When we request the passenger information, we’ll get the human readable value. We will also be safe from enum renames breaking the frontend code. The frontend can request allValues and get the full human readable list for displaying in drop-downs, etc.

On second glance, there’s an implicit assumption here: We are equating the @JsonProperty with the display name of the value!

Unfortunately, later on, another business process decided to save the Passenger record to the database. Incidentally, the selected method for saving the data was to save it as a single document, so the Passenger record was serialized into json and saved. As part of this serialization, our enum was serialized into its @JsonProperty value.

Congratulations! We now have a hardwired link between the data saved in the DB and the data displayed in the app.

We can still rename the enum, but on the day we will decide to localize the text, we’re going to find out that we can’t just do it. We now have to first run a project of migrating the DB value into something stable and decoupling it from the app’s displayed value.

What would have been a better approach?

Instead of overloading the use of @JsonProperty, we could have defined a dedicated field for the display name:

enum class MealPreference(val displayName: String) {
  NONE("None"),
  VEGETARIAN("Vegetarian"),
  VEGAN("Vegan"),
  LACTO_OVO("Lacto/Ovo"),
  KOSHER("Kosher"),
  INDIAN("Indian");

  val displayValues
    get() = values().map { it.displayName }
}

The app has a dedicated field, and it can get a list of values via displayValues.

When saving to the DB, we save the enum value. That’s still not ideal, since a rename in the enum might break the DB data, so let’s decouple that too by adding a dbValue field:

enum class MealPreference(val displayName: String, val dbValue: String) {
  NONE(displayName = "None", dbValue = "none"),
  VEGETARIAN(displayName = "Vegetarian", dbValue = "vegetarian"),
  VEGAN(displayName = "Vegan", dbValue = "vegan"),
  LACTO_OVO(displayName = "Lacto/Ovo", dbValue = "lacto_ovo"),
  KOSHER(displayName = "Kosher", dbValue = "kosher"),
  INDIAN(displayName = "Indian", dbValue = "indian");

  companion object {
    private val dbValueToEnum = values().associateBy { it.dbValue }

    fun fromDbValue(value: String): MealPreference = dbValueToEnum.getValue(value)
  }

  val displayValues
    get() = values().map { it.displayName }

}

When serializing the Passenger record, we need to make sure that the dbValue is used. To that purpose, we define an internal class that will represent the DB record:

data class Passenger(
    val firstName: String,
    val lastName: String,
    val emailAddress: String,
    val mealPreference: MealPreference
) {

  companion object {
    fun fromDbRecord(dbRecord: DbRecord) = dbRecord.toPassenger()
  }
  
  fun toDbRecord() = DbRecord(firstName, lastName, emailAddress, mealPreference.dbValue)

  data class DbRecord(
      val firstName: String,
      val lastName: String,
      val emailAddress: String,
      val mealPreference: String
  ) {
    fun toPassenger() = Passenger(firstName, lastName, emailAddress, MealPreference.fromDbValue(mealPreference))
  }
}

We now have complete decoupling between the app representation, the business logic code and the DB record representation.

On the downside, there is more boilerplate here – we’re defining a copy of the Passenger class that is only used as a simple DB record, but in my opinion the savings in future development time and reduced decoupling make up for this downside.

As an additional side-benefit, the Passenger class is now free to change structure if we need to do so, without breaking the DB record. For example, Passenger could change to this in the future, with no need to run a DB migration:

data class Passenger(
    val person: Person,
    val mealPreference: MealPreference
) {

  companion object {
    fun fromDbRecord(dbRecord: DbRecord) = dbRecord.toPassenger()
  }

  fun toDbRecord() = DbRecord(person.firstName, person.lastName, person.emailAddress, mealPreference.dbValue)

  data class DbRecord(
      val firstName: String,
      val lastName: String,
      val emailAddress: String,
      val mealPreference: String
  ) {
    fun toPassenger() = Passenger(
        Person(firstName, lastName, emailAddress),
        MealPreference.fromDbValue(mealPreference)
    )
  }
}

Notice that DbRecord remains unchanged. No DB migration is necessary!

Kotlin code styles for stopping loop iterations

This post follows https://galler.dev/getting-background-tasks-to-play-nicely-with-deployments/ and discusses different Kotlin code styles we can use for stopping a long running task. It is a summary of a discussion we had over a PR in which we wanted to terminate an execution loop over a collection.

Iterating over a collection in Kotlin

There are many ways to iterate over collections in Kotlin. Assuming you also want to collect results as you iterate, you can run the gamut with syntax styles ranging from the very Java-esque to super streamlined functional code.

Naive iteration on a collection

Let’s take an example of a loop that receives a list of customer IDs and sends them an email. Our boilerplate will look like this:

class Iterations {

  private val customerIds = List(100) {
    Integer.toHexString(Random.nextInt(256 * 256))
  }

  private fun sendEmail(customerId: String): Boolean {
    println("Sending email to customer $customerId")
    sleep(500)
    return Random.nextInt(2) == 0
  }
}

We have a list of 100 customer ID strings and a function that calls our email provider. The function takes 500ms to complete and returns a boolean success indicator.

Our first pass at creating the send loop is a style that you might see with Java developers making the transition to Kotlin, with little experience in functional programming.

Create a list of responses, iterate with a for loop and add the response to the result list. Return the statuses.

  fun sendEmails1(): List<Boolean> {

    val statuses = mutableListOf<Boolean>()
    for (customerId in customerIds) {
      val response = sendEmail(customerId)
      statuses.add(response)
    }

    return statuses
  }

This is, of course, very verbose, and also makes unnecessary use of a mutable list. We can improve on this quite easily:

  fun sendEmails2(): List<Boolean> {

    val statuses = mutableListOf<Boolean>()
    customerIds.forEach { customerId ->
      val response = sendEmail(customerId)
      statuses.add(response)
    }

    return statuses
  }

We’re still using a mutable list, but using a forEach on the collection. Not much of an improvement. Let’s keep going:

   fun sendEmails3(): List<Boolean> {

    val statuses = customerIds.map { customerId ->
      val response = sendEmail(customerId)
      response
    }

    return statuses
  }

We’ve made the jump from forEach to map which saves us the trouble of allocating a list of results. Now to clean up and streamline the code:

  fun sendEmails4() = customerIds.map { customerId -> sendEmail(customerId) }

The function is inlined. The response variable has been removed. We’ve even removed the explicit return type for brevity. From 6 lines of code we are down to 1.

As a side-note, implicit return types and putting everything in one line are not necessarily Best Practice. I recommend watching Putting Down the Golden Hammer from KotlinConf 2019 for an interesting discussion on that. This is just an example taken to the absolute extreme for the sake of discussion.

As far as performance, all implementations will take ~50 seconds to run (100 x 500ms).

A wrench in the gears

Now comes the twist: We have to be able to stop mid-way. We are shutting down our process and we need to interrupt our run.

Let’s expand our boilerplate to include a shutdown flag:

class Iterations {

  private val customerIds = List(100) {
    Integer.toHexString(Random.nextInt(256 * 256))
  }

  private val shouldShutDown = AtomicBoolean(false)

  private fun isShuttingDown(): AtomicBoolean {
    sleep(200)
    return shouldShutDown
  }

  init {
    Thread {
      sleep(3000)
      shouldShutDown.set(true)
    }.start()
  }


  private fun sendEmail(customerId: String): Boolean {
    println("Sending email to customer $customerId")
    sleep(500)
    return Random.nextInt(2) == 0
  }

This shutdown flag comes with a cost – 200ms of performance. In the example, the flag is in-memory, but it could also be a call to the DB or an external API. The list of customers could be 100K items long, not 100. Therefore we are simulating a time penalty for the flag check. We also initialize a background thread that will raise the shutdown flag after 3 seconds. Total time for each iteration will be 700ms (500 for sending the email + 200 for checking the flag)

How can we add the check to our code?

Let’s try and make a minimal change:

  fun sendWithInterrupt1(): List<Boolean> = customerIds.mapNotNull { customerId ->
    if (isShuttingDown().get()) return@mapNotNull null

    sendEmail(customerId)
  }

We’re still using map. Sort of. We just modified it to mapNotNull. We’ve made the minimal change, and the code is still relatively clean and readable. But mapNotNull will evaluate the entire collection. After 3 seconds, we have processed 5 customers (3000ms / 700ms rounded up). We will pass through 95 more redundant iterations that will cost us about 19 seconds to pass through – 95 calls x 200ms to check the flag. This is unacceptable. We should stop immediately.

The solution that’s staring us in the face is to abandon our map and go back to using forEach:

  fun sendWithInterrupt2(): List<Boolean> {

    val statuses = mutableListOf<Boolean>()
    customerIds.forEach { customerId ->
      if (isShuttingDown().get()) return statuses

      val response = sendEmail(customerId)
      statuses.add(response)
    }

    return statuses
  }

This is ugly. We have a return in two places. We’re back to a mutable list. But we will stop after 3.5 seconds. We have traded readability and elegance in return for performance.

Is there a way to eat our cake and have it too (or is it having your cake and eating it too)? Anyway, we want to stick to a functional style, but also be able to stop mid-evaluation. Turns out that there is a way to do this in Kotlin: Sequences.

Quick reminder here: Kotlin collections are eagerly evaluated by default. Sequences are lazily evaluated.

Let’s see what we can do:

  fun sendWithInterrupt3() = customerIds
      .asSequence()
      .map { customerId ->
        sendEmail(customerId)
      }
      .takeWhile { !isShuttingDown().get() }
      .toList()

We added asSequence before map. The flag check is in the takeWhile section. This acts as our stop criteries – a While condition. We wrap it up with toList().

Now, the only terminal operation here is the toList. All other operations return an intermediate sequence that is not immediately evaluated. When we run this, total running time is about 3.5 seconds – the time it takes to process 5 email sends and flag checks.

What’s the right way to go? (A.K.A. Tradeoffs)

Looking at this final version, I can’t say wholeheartedly that it is the ideal solution. Yes, the style is very functional and looks like a pipeline, but on the other hand, another developer looking at this code may not be familiar with these lesser knows operators (takeWhile, asSequence). We are adding a slight learning curve.

We did manage to avoid mutable lists and intermediate variables, keeping the code tight and leaving less room for future modifications that might lead us down a dark path.

The code is definitely more Kotlin-y and more idiomatic, but some might argue that the KISS principle should guide us to stick with the mutable, non-functional version.

Overall, my personal preference would be to go with this last version, but my recommendation to the engineer who was responsible for the change was to make the call himself according to his preference. There are more important hills to die on ¯\_(ツ)_/¯

Getting background tasks to play nicely with deployments

This article discusses the shutdown of background tasks during a rolling deployment.

The backstory

Early on, when our team was still small, we would occasionally run into the following error in production:

java.lang.IllegalStateException: ServiceLocatorImpl(__HK2_Generated_0,0,1879131528) has been shut down 
at org.jvnet.hk2.internal.ServiceLocatorImpl.checkState(ServiceLocatorImpl.java:2288) 
at org.jvnet.hk2.internal.ServiceLocatorImpl.getServiceHandleImpl(ServiceLocatorImpl.java:629) 
at org.jvnet.hk2.internal.ServiceLocatorImpl.getServiceHandle(ServiceLocatorImpl.java:622) 
at org.jvnet.hk2.internal.ServiceLocatorImpl.getServiceHandle(ServiceLocatorImpl.java:640) 
at org.jvnet.hk2.internal.FactoryCreator.getFactoryHandle(FactoryCreator.java:103) 
... 59 common frames omitted 
Wrapped by: org.glassfish.hk2.api.MultiException: A MultiException has 1 exceptions. They are:

We quickly realized that this error was due to a long-running job that was in the middle of its run during deployment. A service would shut down, the DI framework would kill various components and the job would end up trying to use a decomissioned instance of a class.

Since the team was still small and everyone in the (single) room was aware of the deployment, solving this was put on the back burner – a known issue that was under our control. We had bigger fish to free *.

Eventually, the combination of team size growth and the move to continuous deployment meant that we could no longer afford to ignore this error. An engineer on shift who is no longer familiar with the entire system cannot be relied on to “just know” that this is an “acceptable” error (I would add several more quotes around “acceptable” if I could). The deploys also reached a pace of dozens every day, so we were much more likely to run into the error.

Let’s dive into some more detail about the issue we faced.

The graceful shutdown problem

During deployment, incoming external connections are handled by our infrastructure. Connections are drained using the load balancer to route connections and wait for existing connections to close.

Scheduled tasks are not triggered externally and are not handled by the infrastructure. They require a custom applicative solution in order to shut down gracefully.

The setting

The task we’re talking about is a background task, triggered by Quartz and running in Dropwizard with an infrastructure of AWS Elastic Beanstalk (EBS) that manages deployments. Our backend code is written in Kotlin, but is simple enough to understand even if you are not familiar with the language.

Rolling deployments

Rolling (ramped) deploys are a deployment method where we gradually take a group of servers that run a service offline, deploy a new version, wait for the server to come back online and move on to the next server.

This method allows us to maintain high availability while deploying a new version across our servers.

In AWS’ Elastic Beanstalk (EBS), you can configure a service to perform a rolling deploy on the EC2 instances that run a service. The load balancer will stop routing traffic to an EC2 instance and wait for open connections to close. After all connections have closed (or when we reach a max timeout period), the instance will be restarted.

The load balancer does not have any knowledge of what’s going on with our service beyond the open connections and whether the service is responding to health checks.

Shutting down our background tasks

Let’s divide what needs to be done into the following steps:

  1. Prevent new background jobs from launching.
  2. Signal long running jobs and let them know that we are about to shut down.
  3. Wait for all jobs to complete, up to a maximum waiting period.

Getting the components to talk to each other

The first challenge that we come across is triggering the shutdown sequence. EBS is not aware of the EC2 internals, and certainly not of Quartz jobs running in the JVM. All it does, unless we intervene, is send an immediate shutdown signal.

We need to create a link between EBS and Quartz, to let Quartz know that it needs to shut down. This needs to be done ahead of time, not at the point at which we destroy the instance.

Fortunately, we can use Dropwizard’s admin capabilities for this purpose. Dropwizard enables us to define tasks that are mounted via the admin path by inheriting the abstract class Task. Let’s look at what it does:

class ShutdownTask(private val scheduler: Scheduler) : Task("shutdown") {
 
  override fun execute(parameters: Map<String, List<String>>, output: PrintWriter) {
    // Stop triggering new jobs before shutdown
    scheduler.standby()

    // Wait until all jobs are done up to a maximum of time
    // This is in order to prevent immediate shutdown that may occur if there are no open connections to the server
    val startTime = System.currentTimeMillis()
    while (scheduler.currentlyExecutingJobs.size > 0 && System.currentTimeMillis() - startTime < MAX_WAIT_MS) {
      sleep(1000)
    }
  }
}

Some notes about the code:

  1. The task receives a reference to the Quartz scheduler in its constructor. This allows it to call the standby method in order to stop the launch of new jobs
  2. We call standby, not shutdown, so that jobs that are running will be able to complete their run and save their state in the Quartz tables. shutdown would close the connection to those tables.
  3. We wait up to MAX_WAIT_MS before continuing. If there are no running jobs, we continue immediately.
  4. EBS does not have a minimum time window during which it stops traffic to the instance. If there are no open connections to the process, EBS will trigger a shutdown immediately. This is why we need to check for running jobs and wait for them, not just call the standby method and move on.

Given a reference to Dropwizard’s environment on startup, we can call

environment.admin().addTask(ShutdownTask(scheduler))

to initialize the task. Once added to the environment, we can trigger the task via a POST call to

http://<server>:<adminport>/tasks/shutdown

The last thing we need to do is add this call to EBS’ deployment. EBS provides hooks for the deploy lifecycle – pre, enact and post. In our case we will add it to the pre stage. In the .ebextensions directory, we’ll add the following definition in a 00-shutdown.config file:

files:
  "/opt/elasticbeanstalk/hooks/appdeploy/pre/00_shutdown.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #! /bin/bash
      echo "Stopping Quartz jobs"

      curl -s -S -X POST http://<server>:<adminport>/tasks/shutdown

      echo "Quartz shutdown completed, moving on with deployment."

So far we have accomplished steps #1 & #3 – Preventing new jobs from launching and waiting for running jobs to complete. However, if there are long running jobs that take more than MAX_WAIT_MS to complete, we will reach the time out and they will still terminate unexpectedly.

Signaling to running jobs

We put a timeout of MAX_WAIT_MS to protect our deployment from hanging due to a job that is not about to terminate (we could also put a -max-time on the curl command).

Many long running jobs are batch jobs that process a collection of items. In this step, we would also like to give them a chance to terminate in a controlled fashion – Give them a chance to record their state, notify other components of their termination or perform other necessary cleanups.

Let’s give these jobs this ability.

We’ll start with a standard Quartz job that has no knowledge the state of the system:

class SomeJob: Job {
  override fun execute(context: JobExecutionContext) {
    repeat(1000) { runner ->
      println("Iteration $runner - Doing something that takes time")
      sleep(1000)
    }
  }
}

We want to be able to stop the job. Let’s define an interface that will let us do that:

interface StoppableJob {
  fun stopJob()
}

Now, let’s implement the interface:

class SomeStoppableJob: Job, StoppableJob {

  @Volatile
  private var isActive: Boolean = true

  override fun execute(context: JobExecutionContext) {
    repeat(1000) { runner ->
      if (!isActive)  {
        println("Job has been stopped, stopping on iteration $runner")
        return
      }

      println("Iteration $runner - Doing something that takes time")
      sleep(1000)
    }
  }

  override fun stopJob() {
    isActive = false
  }
}

This setup allows us to pause execution. Notice that our flag needs to be volatile. Now we can modify ShutdownTask to halt the execution of all running tasks:

class ShutdownTask(private val scheduler: Scheduler) : Task("shutdown") {
 
  override fun execute(parameters: Map<String, List<String>>, output: PrintWriter) {
    // Stop triggering new jobs before shutdown
    scheduler.standby()

    scheduler.currentlyExecutingJobs
        .map { it.jobInstance }
        .filterIsInstance(StoppableJob::class.java)
        .forEach {
          it.stopJob()
        }

    // Wait until all jobs are done up to a maximum of time
    // This is in order to prevent immediate shutdown that may occur if there are no open connections to the server
    val startTime = System.currentTimeMillis()
    while (scheduler.currentlyExecutingJobs.size > 0 && System.currentTimeMillis() - startTime < MAX_WAIT_MS) {
      sleep(1000)
    }
  }
}

Conclusion

Looking at the implementation, we now have the ability to control long-running tasks and have them complete without incident.

It reduces noise in production that would otherwise send engineers scrambling to see whether there is a real problem, or just a deployment artefact. This enabled us to focus on real issues and improve the quality of our shifts.

Overall, well worth the development effort.

[update] For a discussion on how to implement shutdown flags in Kotlin, see https://galler.dev/kotlin-code-styles-for-stopping-loop-iterations/

Preventing software antipatterns with Detekt

Detekt is a static code analyzer for Kotlin. This post talks about how I used Detekt for non-trivial, systematic improvements to code quality, using custom Detekt rules.

Source files can be found here: https://github.com/ygaller/detekt-antipatterns

Antipatterns, antipatterns everywhere

I review a lot of pull requests on a regular basis. During reviews, antipatterns show up all the time. Many of them are repetitive and addressing them is important, but does not involve deep insight into the code. In fact, repetitive nitpicking in code reviews is an antipattern in itself. Repeating the same points over and over is exasperating, and they keep popping up in PRs coming from different developers.

Sometimes you see an antipattern that is so bad that it sends you on a gardening task to weed out all instances of it in the codebase. One such example is the instantiation of Jackson’s ObjectMapper.

Antipattern example – misusing ObjectMapper

Instantiating an ObjectMapper is intensive as far as object allocation and memory use. If you’re not careful and causally use it in code that is called frequently, it will cause memory churn – the intensive allocation and release of objects, leading to frequent garbage collection. This can directly contribute to the instability of a JVM process.

Let’s have a look at an example of bad usage of the ObjectMapper: We have a CustomerRecord class which represents a DB record. This record has a json string field. When we fetch the record we want to convert the record into a DTO. This requires us to deserialize the string so that we can access the serialized object.

data class CustomerRecord(
    val firstName: String,
    val lastName: String,
    val emailAddress: String,
    val preferences: String // json
) {
 
  fun toDto(): CustomerDto {
    val preferences = ObjectMapper().registerModule(KotlinModule()).readValue<CustomerPreferences>(preferences)
    return CustomerDto(
        firstName,
        lastName,
        emailAddress,
        preferences.language,
        preferences.currency
    )
  }
}

The problem here is that with each invocation of toDto(), we are creating a new instance of ObjectMapper.

How bad is it? Let’s create a test and compare performance. For comparison, version #2 of this code has a statically instantiated ObjectMapper:

data class CustomerRecord(
    val firstName: String,
    val lastName: String,
    val emailAddress: String,
    val preferences: String // json
) {
  companion object {
    private val objectMapper = ObjectMapper()
        .registerModule(KotlinModule()) // To enable no default constructor
  }

  fun toDtoStatic(): CustomerDto {
    val preferences = objectMapper.readValue<CustomerPreferences>(preferences)
    return CustomerDto(
        firstName,
        lastName,
        emailAddress,
        preferences.language,
        preferences.currency
    )
  }
}

Let’s compare performance:

IterationsNew Duration
(Deserializations / s)
Static Duration
(Deserializations / s)
Ratio
10K767ms (13,037)44ms (227,272)x17
100K4675ms (21,390)310ms (322,580)x15
1M37739ms (26,504)1556ms (642,673)x24
New – creating ObjectMapper() in each iteration
Static – creating ObjectMapper() once

This is an amazing result! Extracting the ObjectMapper to a static field and preventing an instance from being created each time has improved performance by a factor of over 20 in some cases. This is the ratio of time that initializing an ObjectMapper takes vs. what the actual derserialization takes.

The problem with the one-time gardener

A large codebase could have hundreds of cases of bad ObjectMapper usages. Even worse, a month after cleaning up the codebase and getting rid of all these cases, some new engineers will join the group. They are not familiar with the best practice of static ObjectMappers and inadvertently reintroduce this antipattern into the code.

The solution here is to proactively prevent engineers from committing such code. It should not be up to me, the reviewer to catch such errors. After all, I should be focusing on deeper insights into the code, not on being a human linter.

Detekt to our aid

To catch these antipatterns in Kotlin, I used a framework called Detekt. Detekt is a static code analyzer for Kotlin. It provides many code smell rules out of the box. A lesser known feature is that it can be extended with custom rules.

Let’s first try and figure out what would valid uses of ObjectMapper look like. An ObjectMapper would be valid if instantiated:

  • Statically, in a companion class
  • In a top level object which is a singleton itself
  • In a private variable in the top-level of a file, outside of a class but not accessible from outside the file

What would an ObjectMapper code smell look like? When it’s instantiated:

  • As a member of a class that may be instantiated many times
  • As a member of an object that is not a companion object within a class (highly unlikely but still…)

I created a rule to detect the bad usages and permit the good ones. In order to do this, I extended Detekt’s Rule abstract class.

Detekt uses the visitor pattern, exposing a semantic tree of the code via dozens of functions that can be overridden in Rule. Each function is invoked for a different component in the code. At first glance, it’s hard to understand which is the correct method to override. It took quite a bit of trial and error to figure out that the correct function to override is visitCallExpression. This function is invoked for all expressions in the code, including the instantiation of ObjectMapper.

An example of how we examine the expression can be found in class JsonMapperAntipatternRule. We search the expression’s parents for the first class & object instances. Depending on what we find, we examine the expression in the context of a class or of a file.

    val objectIndex = expression.parents.indexOfFirst { it is KtObjectDeclaration }
    val classIndex = expression.parents.indexOfFirst { it is KtClass }

    val inFileScope = objectIndex == -1 && classIndex == -1
    if (inFileScope) {
      validateFileScope(expression)
    } else {
      validateObjectScope(objectIndex, classIndex, expression)
    }

Test coverage can be found in JsonMapperAntipatternRuleTest. Tests include positive and negative examples and can be added as we encounter more edge cases that we may not have thought of.

The positive test cases we want to allow:

// Private ObjectMapper in class' companion object
class TestedUnit {

  companion object {
    private val objectMapper = ObjectMapper()
  }
}
// ObjectMapper in object
object TestedObject {
    private val objectMapper = ObjectMapper()
}
// Private ObjectMapper in file (top level)
private val objectMapper = ObjectMapper()

class TestedObject {
}
// Part of a more complex expression
private val someObject = ObjectMapper().readValue("{}")

class TestedObject {
}

On the other hand, cases which we want to prevent:

// ObjectMapper not in companion class
class TestedUnit {

  private val objectMapper = ObjectMapper()
}
// Within a non-companion object
class External {
  object Internal {
      val notGood = ObjectMapper()
  }
}
// Not private in top level of file
val notGood = ObjectMapper()

class External {
}

The detekt-custom-rules module is a minimal Detekt plugin module. Once built, the result is a jar that can be used interactively within IntelliJ, or as a plugin when building the rest of the project.

Using the plugin with IntelliJ

After installing the Detekt plugin in IntelliJ, we enable it and point it to the plugin jar like so:

The result is that we now have syntax highlighting for this issue:

Using the plugin with a Maven build

To use is as part of the build, we need to point the maven plugin to the jar:

            <plugin>
                <groupId>com.github.ozsie</groupId>
                <artifactId>detekt-maven-plugin</artifactId>
                <version>${detekt.version}</version>
                <executions>
                    <execution>
                        <id>detekt-check</id>
                        <phase>verify</phase>
                        <goals>
                            <goal>check</goal>
                        </goals>
                    </execution>
                </executions>

                <configuration>
                    <config>config/detekt.yml</config>
                    <plugins>
                        <plugin>detekt-custom-rules/target/detekt-custom-rules-1.0-SNAPSHOT.jar</plugin>
                    </plugins>
                </configuration>
            </plugin>

With the plugin in place we receive a warning during the build:

custom-rules - 5min debt
        JsonMapperAntipatternRule - [preferences] at detekt-antipatterns/customers/src/main/java/com/ygaller/detekt/CustomerRecord.kt:18:23

In detekt.yml it’s possible to configure the build to either fail or just warn on the custom rule.

Conclusion

Using Detekt and the custom rule, I managed to screen for a non-trivial antipattern. Adding it to IntelliJ and to the build means that it is much less likely to pop up in future code reviews.

Moreover, it decenteralizes the knowledge of this issue and enables any engineer working on our code in the future to be alerted to the fact that he is introducing an inefficiency into the system. One that can easily be avoided.