Sentinel value Vs. null for id column

Right now we have a lot of Entity classes with nullable id and a lot of fields that are nullable, but not really after creation.

I know that in many cases we can use projections and DTOs with correct nullability when querying from the DB. But sometimes for convenience the Entity class itself is used, and it would be good if the nullability is correct.

As far as I see, in Kotlin, we can:

  1. Use a non-null sentinel value
  2. Use nullable backing field with non-null accessor

What is the “recommended” approach?

Is there any new way I have not mentioned here that is better?

AI thoughts:

  1. Generated properties should be non-nullable with default values - Hibernate will override them on persist

  2. Use val (not var) - prevents accidental modification after generation

  3. Place generated properties in the class body, not the constructor - separates “inputs” from “outputs”

@Entity
class Author(
    var name: String  // Constructor param - mutable input
) {
    @Id 
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0  // Body - generated, immutable, non-nullable
    
    @CreationTimestamp
    val createdAt: Timestamp = Timestamp(0)  // Body - generated, immutable
}

Does This Work for Your Case?

Partially. Here’s the breakdown:

Field Article’s Approach Works? Notes
id :white_check_mark: Yes val id: Long = 0 works great
createdAt :white_check_mark: Yes val createdAt: Instant = Instant.EPOCH
tenantId :warning: Tricky Not DB-generated, but set by Hibernate’s @TenantId filter
providerType :cross_mark: No Required input, use lateinit var
companyBankId :white_check_mark: Already doing it You have var companyBankId: Long = -1

Applied to Your Payment Entity

@Entity
@Table(name = "payments")
class Payment(
    // Constructor params - required inputs
    providerType: PaymentProviderType,
    providerPaymentId: String,
    paymentAmount: BigDecimal,
    currency: String,
    companyBankId: Long
) {
    // Generated/managed by framework - non-nullable with defaults
    @Id
    @BatchSequence(name = "payments_id_seq")
    @Column(name = "id", nullable = false)
    val id: Long = 0
    
    @TenantId
    @Column(name = "tenant_id", nullable = false, updatable = false)
    val tenantId: Long = 0  // Set by Hibernate tenant filter before persist
    
    @CreationTimestamp
    @Column(name = "created_at", nullable = false, updatable = false)
    val createdAt: Instant = Instant.EPOCH
    
    @UpdateTimestamp
    @Column(name = "updated_at", nullable = false)
    val updatedAt: Instant = Instant.EPOCH
    
    // Mutable fields from constructor
    @Column(name = "provider_type", nullable = false, length = 50)
    @Enumerated(EnumType.STRING)
    var providerType: PaymentProviderType = providerType
    
    @Column(name = "provider_payment_id", nullable = false, unique = true)
    var providerPaymentId: String = providerPaymentId
    
    // ... etc
}

Caveat: The 0 Sentinel Problem

The one downside is that id = 0 before persistence could theoretically be confused with a real ID. In practice:

  • Most DBs start sequences at 1

  • You can add a helper if paranoid:

val isPersisted: Boolean get() = id != 0L

Verdict

Yes, use this approach. It’s cleaner than nullable types with !! everywhere, and Hibernate handles it correctly. The article’s advice is sound and widely adopted.

You can’t make generated properties non-null in entities, since the values are only set at a later stage. Faking the non-nullness with sentinel values might work, but still, you can’t mark the properties as val as that implies “finality” i.e. you can’t change the values according to JVM rules.
If it is important to you that the object model nullability matches the constraints of the database, I would suggest you to use DTOs/projections.
Any “hacks” to workaround this mismatch is probably going to bite you back at some point and introduce “technical debt” in one way or another.