Robust and convenient Kotlin primitives

How often do you get into this situation?

Kotlin
fun Document.isAccessible(tenantId: Int, userId: Int): Boolean

if (doc.isAccessible(userId, tenantId)) ... // Error or security breach

I hope – never – because you always add parameter names to function calls and also have 100% test coverage.

Still, maintaining hundreds of indistinguishable val id: Int properties is frustrating and inevitably leads to mistakes.

There’s a nice trick to solve this problem once and for all.

Kotlin
@JvmInline // Required for JVM world
value class TenantId(val value: Int)

Now you have a custom type for your Int id with almost no overhead. (“Almost” because Kotlin still might box value class in case if it’s used as a type parameter or a nullable value)

Now the dangerous code above will just fail to compile:

Kotlin
if (doc.isAccessible(userId, tenantId)) ...
// Argument type mismatch: actual type is 'UserId', but 'TenantId' was expected.

To make it easier to convert Ints to custom classes use helpers like:

Kotlin
fun Int.toTenantId() = TenantId(this)

It appeared to be so helpful that now I also use custom value classes for UUIDs and Strings:

Kotlin
@JvmInline
value class DocumentReference(val value: String)

data class Document(
  val ref: DocReference,
  ...
)

@JvmInline
value class ExternalResourceId(val value: UUID)

data class ExternalResource(
  val id: ExternalResourceId,
  ...
)

You can go further and make your data framework understand this custom types. For example jOOQ:

Kotlin
class TenantIdConverter : AbstractConverter<Int, TenantId>(Int::class.java, TenantId::class.java) {
    override fun from(v: Int?): TenantId? = v?.toTenantId()

    override fun to(tenantId: TenantId?): Int? = tenantId?.value
}

// Then in jooq configuration
forcedType {
    includeExpression = "tenant\\.id"
    userType = "org.example.TenantId"
    converter = "org.example.TenantIdConverter"
}
                    
forcedType {
    includeExpression = ".*\\.tenant_id"
    userType = "org.example.TenantId"
    converter = "org.example.TenantIdConverter"
}

Now you could save and fetch your TenantId directly.

Moving away from primitives is a big step towards Domain-Driven Design and Hexagonal Architecture which in my opinion is essential for any enterprise project!

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *