One day, I had a lively discussion with colleagues of mine about using custom types as DTO model ID. I wanted to share some details of it with you.
Let's imagine we have an entity corresponding to a table with the same name in a database.
@Table(value = "employee")
data class EmployeeEntity(
@Id
val id: Long? = null,
@Column("name")
val name: String,
)
I used to describe the DTO model that corresponds to this entity something like this:
data class Employee(
val id: Long,
val name: String,
)
The same model written using the discussed approach with custom ID:
data class Employee(
val id: Id<Employee>,
val name: String,
) {
// from 100 lines of code to make it work
}
These two models differ in the id
field. In the first one, the id
is a simple Long. In the second one, it's a custom type parameterized by the entity type (such as an Ouroboros). Here's how this custom Id
type might look:
class Id<T : Any> private constructor(
private var _value: Long?,
) {
// from 100 lines of boiler plate code
// to override standard functions,
// implement builders and etc.
}
Besides writing 100+ lines of code for this custom ID to work, there are at least another 100+ lines of code implementing factories and builder methods to convert the model to an entity and vice versa.
The main question in our discussion was whether it's better (more effective) to use val id: Long
or val id: Id<Employee>
.
Advantages of the first approach (val id: Long
):
- it's Kotlin,
- it's simple and easy to understand,
- no unnecessary code that doesn't carry business logic. Less code means less mental load, faster feature development, fewer mistakes, and less testing.
Cons:
- you might accidentally pass the wrong ID to a function. For example:
val employee = getEmployeeById(employeeId = organization.id)
fun getEmployeeById(employeeId: Long) {...}
- this approach might seem too simple for some.
An advantage of the second approach (val id: Id<Employee>
):
- it's some harder to make mistakes. Actually, this was the main argument of my opponents: you can't even trust yourself, everyone can make mistake,
- a dose of dopamine for anyone who missed complexity in their project.
Cons:
- it's still possible to accidentally make a mistake
val employee = getEmployeeById(
employeeID = Id.from(organization.id)
)
fun getEmployeeByID(id: Id<Employee>) {...}
- this is not Kotlin style code. Perhaps my colleagues used this approach in Java code. And it was necessary in their Java projects. But Kotlin is the language designed to make complex things easy, not vice versa. To avoid accidentally confusing parameters, Kotlin allows passing parameters by name:
val employee = getEmployeeById(employeeId = 1L)
So, what's the verdict?
If you can accidentally make a mistake using both approaches, then why write more code to use a custom type as an ID?
Kotlin is a well-designed and elegant language. Dragging the baggage of Java habits into Kotlin code is like voluntarily abandoning the new and efficient for the old and clunky.
Top comments (0)