I was recently implementing the repository pattern when I noticed a potential bug. For those who want a short explanation of the repository pattern, you basically take however you’re getting your data, and put the implementation behind an interface. It’s useful for mocking out databases in tests and it cleanly separates your data access logic from your other code.

For example, this is the interface for my PlaylistRepository, the interface I’m now using for interacting with playlists in Auracle Music Player

The potential bug with this method is a little subtle, but it could have really weird effects for the user…

The implementation of PlaylistRepository that I use in production is RealmPlaylistRepository, which is backed by a Realm database, and I store both the playlist ID and the song ID as longs.

It makes sense to store these values as longs in a database, but this means that a playlist ID and a song ID are both recognized by the compiler as having the same type, although they’re not logically the same thing.

This means that the compiler wouldn’t complain if I accidentally switched around the values at a call site like so:

Logically this doesn’t make any sense, but the compiler is perfectly content with this buggy code. It will successfully build, and it will betray you and it will allow you to ship that buggy code.

How can I clean up my code so the compiler can catch silly bugs like this?

I’m glad you asked!

Compile Time Safety

What we’re trying to do here is also known as compile time safety. We want to fail the build whenever we pass in the wrong type of id. This will be similar to how Kotlin has nullability built into the type system so that you get compile time errors instead of runtime errors for de-referencing null.

To fail the build when the compiler encounters this logical error, we’re going to make new types!

We’ll start off with the following classes:

Then we’ll change the parameters of our function in PlaylistRepository so that it has the right logical types:

Since this method accepts strongly typed arguments: a SongId and a PlaylistId, if you tried to do the following, the compiler will fail the build.

Now we have our compile time safety, but we’re not done. We still need to actually generate these types somehow. I didn’t want to modify my model classes to add this functionality, so I decided to make extension properties.

This allows us to do the following at the call site:

Now we can rest assured that the compiler will fail the build if we ever try to pass in a SongId as a PlaylistId!

But can we expand this to further take advantage of types? Of course!

In the Auracle code base, it’s possible for a Song to have an invalid ID if it hasn’t been added to the database yet. Previously I was using an ID of -1 to represent this state, but we can do much better with types.

Here’s the updated version of SongId where the type system takes into account that a SongId can be invalid:

The first difference is that we’ve turned SongId into a sealed class.

The Kotlin docs give the following definition for sealed classes:

Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type. They are, in a sense, an extension of enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances which can contain state.

The way we’ve defined SongId.kt, a SongId can either be a ValidSongId with an ID property representing the actual long value for the ID, or it can be an InvalidSongId that has no ID property.

The key benefit of using sealed classes comes into play when you use them in a when expression. If it’s possible to verify that the statement covers all cases, you don’t need to add an else clause to the statement. However, this works only if you use when as an expression (using the result) and not as a statement.

This allows us to write the following code in RealmSongRepository :

More on smart casting here

As a bonus, the IDE will now auto-complete all the cases when we switch on a SongId, or any other sealed class for that matter.

Next I’d like to point out that InvalidSongId is declared as an object and not a class. Kotlin’s object keyword allows us to do object declaration, a built-in way of creating singletons. Since we’re declaring an object and not a class, there can only be one instance of InvalidSongId, which makes sense logically.

And with that, we’ve built ourselves a type system! I’m not ashamed to admit that when I implemented this change, I caught a case where I was passing in the wrong type of id. If you’ve created your own custom type system for yourself to fix/prevent bugs, leave a comment below!

Thanks for reading :)