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. This is lesson 4 of 4. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the Kotlin Academy learning path, and your progress syncs across the web and the CoddyKit app. The Kotlin Academy course includes 4 lessons in total.
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
Learn Kotlin with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 51
- Lessons
- 203
Frequently Asked Questions
Is the “Functional Domain Modeling with Arrow's Core Types” lesson free?
Yes — any 3 lessons of the Kotlin Academy learning path, including “Functional Domain Modeling with Arrow's Core Types”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The Kotlin Academy course includes 4 lessons in total.
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, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.
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