Functional Domain Modeling with Arrow's Core Types
Model complex domains using Either, NonEmptyList, and validated error accumulation.
Functional Domain Modeling with Arrow's Core Types is a free Kotlin Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Kotlin Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Is Functional Domain Modeling?
Functional domain modeling uses algebraic types (Either, Option, sealed classes) to encode business rules into types. Invalid states become unrepresentable at compile time, and errors are explicit in function signatures — no hidden exceptions.
Value Objects with Inline/Value Classes
Use Kotlin value classes to wrap primitives and prevent primitive obsession. The type system rejects passing a UserId where a PostId is expected:
@JvmInline value class UserId(val value: Long)
@JvmInline value class PostId(val value: Long)
fun findUser(id: UserId): Either<UserError, User> = TODO()
// findUser(PostId(1L)) // Compile error!Sealed Classes for Domain Errors
Model errors as sealed class hierarchies. Each subclass carries only the data relevant to that error case:
sealed class UserError {
data class NotFound(val id: UserId) : UserError()
data class EmailTaken(val email: String) : UserError()
data class ValidationFailed(val field: String, val reason: String) : UserError()
}Validated: Accumulating Multiple Errors
When you want to report all validation errors at once (not just the first), use Arrow's Validated (or zipOrAccumulate in Raise DSL). It accumulates errors into a NonEmptyList:
import arrow.core.Validated
import arrow.core.valid
import arrow.core.invalid
fun validateName(name: String): Validated<String, String> =
if (name.isNotBlank()) name.valid() else "Name is blank".invalid()Composing Validations
Use zip() to combine multiple Validated results. If any is Invalid, all errors are accumulated:
val result: Validated<NonEmptyList<String>, User> =
validateName(name).zip(validateEmail(email)) { n, e -> User(n, e) }
.mapLeft { it }Making Illegal States Unrepresentable
Instead of a User with a nullable email and a flag isVerified, use sealed subclasses so the type system prevents accessing the email of an unverified user:
sealed class User {
data class Unverified(val id: UserId, val pendingEmail: String) : User()
data class Verified(val id: UserId, val email: String) : User()
}Smart Constructors
Use companion object factory functions that return Either or Option instead of public constructors. This ensures instances are always in a valid state:
class Email private constructor(val value: String) {
companion object {
fun of(raw: String): Either<String, Email> =
if (raw.contains("@") && raw.length <= 255) Email(raw).right()
else "Invalid email format".left()
}
}Combining Either and Option in a Pipeline
A real domain operation typically chains repository lookups (Option) with validations (Either). Use Arrow's either { } block with .bind() to compose them fluently:
fun createPost(authorId: UserId, title: String, body: String): Either<PostError, Post> = either {
val author = userRepo.findById(authorId).toEither { PostError.AuthorNotFound }.bind()
val validTitle = validateTitle(title).bind()
postRepo.save(Post(author, validTitle, body))
}Immutability and Copy
Domain entities should be immutable. Use data classes and copy() to derive new states. Arrow's Lens (from arrow-optics) enables ergonomic nested updates without mutation:
val updated = user.copy(email = "new@example.com")
// Arrow Optics:
val emailLens = User.email
val updatedWithLens = emailLens.set(user, "new@example.com")Typesafe Configuration with Sealed Classes
Model application configuration variants with sealed classes so the compiler forces you to handle each case:
sealed class DbConfig {
data class Postgres(val url: String, val user: String, val pass: String) : DbConfig()
data class InMemory(val dbName: String = "test") : DbConfig()
}Benefits at a Glance
Functional domain modeling with Arrow's core types delivers:
- No null pointer exceptions in domain code
- Errors visible in function signatures
- Exhaustive
whenchecks on sealed hierarchies - Testable pure functions
- Self-documenting code
Quick Check
What is the primary benefit of using a sealed class hierarchy for domain errors over throwing exceptions?
Recap: Functional Domain Modeling with Arrow's Core Types
Key takeaways:
- Value classes prevent primitive obsession
- Sealed error hierarchies make failure cases exhaustive and type-safe
- Smart constructors guarantee valid instances
either { }.bind()composes fallible operations cleanly- Immutable data +
copy()/ Arrow Optics for safe state transitions
Frequently asked questions
Is the “Functional Domain Modeling with Arrow's Core Types” lesson free?
Yes — the full text of “Functional Domain Modeling with Arrow's Core Types” is free to read here on the web, and the Kotlin Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Kotlin Academy course, upgrade to CoddyKit PRO.
What will I learn in “Functional Domain Modeling with Arrow's Core Types”?
Model complex domains using Either, NonEmptyList, and validated error accumulation. You practise Kotlin Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Kotlin Academy?
No prior experience is required. Kotlin Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Functional Domain Modeling with Arrow's Core Types” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Kotlin Academy lesson?
Yes. Every Kotlin Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Either : Typed Error Handling Without Exceptions
- Arrow Raise DSL: Composing Typed Errors
- Option and Nullable: When to Use Each
- Functional Domain Modeling with Arrow's Core Types