Tag: Value class

  • 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!