Why DDD and Kotlin Are the Perfect Pair
When a project grows and business requirements start to resemble a tangled mess, the usual CRUD approach stops working. You catch yourself thinking that half the code consists of endless if-else checks, and any change in logic leads to modifying a dozen files. Sound familiar?
That's where Domain-Driven Design (DDD) steps in — an approach that places business logic at the center of the universe. And Kotlin, with its concise syntax, powerful type system, and support for functional concepts, becomes the ideal tool for implementing DDD. In this article, we'll explore how to combine these two forces so your code becomes not just functional, but expressive and flexible.
If you write in Kotlin and want your architecture to breathe rather than creak — you've come to the right place. Today, we won't just talk about theory; we'll write real code examples.
Domain-Driven Design: A Quick Primer for the Busy
Before diving into code, let's agree on terms. DDD is not a framework or a library. It's a set of principles that help developers and business stakeholders speak the same language.
Key concepts:
- Ubiquitous Language — terms in the code should match the terms used by business analysts and clients.
- Bounded Context — a clear boundary within which a model makes sense. For example, "Customer" in the sales context and "Customer" in the support context are different entities.
- Entity — an object with a unique identifier that changes over time.
- Value Object — an immutable object defined by its attributes, not its identifier.
- Aggregate — a group of related objects treated as a single unit.
- Repository — an abstraction for data access that hides storage details.
Sounds abstract? Let's translate this into Kotlin.
Value Objects in Kotlin: Grace and Safety
Let's start with the most pleasant part — Value Objects (VO). In Kotlin, implementing them is a pleasure thanks to data class.
Imagine we're working on a hotel booking system. We have the concept of a "Stay Period." This is not an entity — it's just a set of dates. A perfect candidate for a VO.
data class StayPeriod( val checkIn: LocalDate, val checkOut: LocalDate) { init { require(checkIn.isBefore(checkOut)) { "Check-in date must be before check-out date" } }
val totalNights: Int get() = ChronoUnit.DAYS.between(checkIn, checkOut).toInt()
fun overlapsWith(other: StayPeriod): Boolean { return !(this.checkOut.isBefore(other.checkIn) || this.checkIn.isAfter(other.checkOut)) }}Why is this great?
- Immutability — all fields are
val. No one will accidentally change the check-in date. - Built-in validation — the
initblock ensures the object is created only in a valid state. - Behavior — the
overlapsWithmethod encapsulates a business rule. You don't look for date overlaps in services; you ask the object itself.
In DDD, we call this a rich model — the object doesn't just store data but can perform business operations.
Entity: When Identity Matters
Now let's move on to Entity. Unlike a VO, an entity changes and has identity. In Kotlin, we use a regular class, but with an emphasis on the identifier.
Example — a hotel booking (Booking):
class Booking( val id: BookingId, val roomNumber: Ro