PUBLIC OBJECT

Farm or Grind

Suppose your organization has a widely-used internal library for validating customer usernames:

private val usernameRegex = Regex("[a-z]{2,40}")

fun isValidUsername(username: String): Boolean {
  return usernameRegex.matches(username)
}

The function becomes widely adopted:

  • Some callers use it during customer sign-up. We don’t want customers putting slashes in their usernames!
  • Others use it to validate the strings they’re receiving are actually usernames. It would be bad to mix up a username with an email address!

Years later, your product team changes the rules. So you implement version 2 of the username rules:

private val usernameRegex = Regex("[a-z0-9-]{2,15}")

fun isValidUsername(username: String): Boolean {
  return usernameRegex.matches(username)
}

But this won’t work! It’ll crash validating long usernames issued before version 2. So you split callers by use-case:

private val usernameRegex2015 = Regex("[a-z]{2,40}")
private val usernameRegex2023 = Regex("[a-z0-9-]{2,15}")

@Deprecated(
  message = "If this is for sign-up, call isValidNewUsername(). " +
    "Otherwise call isValidExistingUsername()"
)
fun isValidUsername(username: String): Boolean {
  return isValidExistingUsername(username)
}

fun isValidExistingUsername(username: String): Boolean {
  return usernameRegex2015.matches(username)
    || usernameRegex2023.matches(username)
}

fun isValidNewUsername(username: String): Boolean {
  return usernameRegex2023.matches(username)
}

Who Fixes The Deprecation Warnings?

Now that you’ve deprecated the old function, somebody needs to do is fix the 400 callers of the old function which spread across 20 product teams.

Option 1: Farm it Out to the Owners of the Calling Code

You merge & release the updated usernames library.

The next day, some engineers who needed valid usernames a few years ago will see the deprecation warning and promptly fix their codebase.

On other teams, the original authors have moved on. The new maintainers will see the deprecation warning and add a task to the next sprint to figure out what do to.

On yet other teams, the team is responsible for some old code that does stuff with usernames. But the team is busy building a replacement service that’ll ship any day now. When that’s done this old code can all be deleted.

In frustration with the laggards, you repeatedly broadcast Slack messages explaining the deprecation and encourage everyone to do their part.

Option 2: You Grind And Fix It Everywhere

You merge & release the updated usernames library.

Next you use a combination of GitHub search, ripgrep and zoekt to find the impacted codebases. You fix ’em all. Sometimes that’s using find & replace, and sometimes it’s just typing.

You need to learn which of the two functions is appropriate for each callsite, but you pattern match and it’s not so bad. “If returning false triggers a customer-visible error message, it might be a sign-up.”

The Case For Grinding

It’s so easy to deprecate and move on. You work in your codebase; other people work in theirs, and there’s no need to learn the style & process conventions for other teams. Deprecation-sensitive downstream teams can fix this quickly. Teams that don’t respond to deprecations are bad software engineers and that’s not your fault!

But farming out work costs more for the organization as a whole.

Developers will need to spend time learning the distinction between the two functions. They may also need to study their own code! But they won’t be working on this problem long enough to discover patterns.

Teams may also spend time prioritizing and scheduling the work. They won’t find out if this is a 5-minute job or a 1-day one until they dig in.

Personally driving a broad change like this to completion is how you can ensure you’ll reach 100%.

Some made up numbers:

  • Farm: 20 teams x 1 day of work per codebase = 20 days
  • Grind: 1 person x 20 codebases x 4 hours of work per codebase = 10 days

The grind approach is also better suited to automation. Maybe you like structured search & replace, or perhaps OpenRewrite.

A Real-World Example

A couple years ago I migrated 300 repos from Gradle’s old Groovy-based syntax to the new Kotlin-based syntax. I had a ton of fun writing a (very crude) program to automate about ~95% of each migration.

The process was fun – I love getting paid to write a parser! And I got to go pretty deep on Gradle.

Now that it’s all .kts a few hundred people will never need to learn Groovy to build their Kotlin services. If instead I’d asked each team to do their own migrations, they’d need somebody to learn Groovy before they could even start.

Fix Your Callers

Most of the time I do deep changes within a single codebase. But sometimes it’s worth making a shallow change in many codebases.