# Make Invalid Domain States Hard to Represent with Kotlin and Arrow

Domain-Driven Design gives us a useful question to ask when modelling a domain:

> What does it mean for this object to be valid?

It is tempting to answer that question with nullable properties and a validation method somewhere in the service layer. The problem is that every caller then has to remember to call that method. Between construction and validation, the object can represent something the domain does not allow.

In this example, we use a blog post as a domain to model. A blog post consists of an ID, title, content, and author. The model is deliberately small, but it demonstrates a pattern that scales well: make domain concepts explicit, keep construction behind a validation boundary, and represent failure in the type returned by the operation.

The result is a domain model that can be used without Spring, a database, or a framework-managed lifecycle. It is just Kotlin code with business rules that are visible at the boundary.

## Start with domain concepts, not primitives

The first step is to stop treating every string as the same thing. A `Title` and a `BlogPostId` may all be represented by strings at runtime, but they are different concepts in the domain.

Kotlin value classes let us express that distinction without introducing the allocation and ceremony of a traditional wrapper class:

```kotlin
@JvmInline
value class BlogPostId(val value: String) {
    override fun toString(): String = value
}

@JvmInline
value class Title(val value: String) {
    override fun toString(): String = value
}
```

This gives the compiler useful information. A function accepting a `BlogPostId` cannot be passed an arbitrary `String` without an explicit conversion. It also communicates intent: this is an identifier, and this is a title.

There is an important nuance here. A value class by itself does not validate its contents, so `Title("")` is still possible in this example. The aggregate factory below is the current invariant boundary. If a title has rules that must hold everywhere, the same pattern can be applied to `Title` itself by hiding its constructor and exposing a validated factory.

Value objects can also represent several related values. For example, a publication window could own the rule that its end date cannot precede its start date:

```kotlin
@ConsistentCopyVisibility
data class PublicationPeriod private constructor(
    val from: LocalDate,
    val to: LocalDate
) {
    operator fun contains(date: LocalDate): Boolean =
        !date.isBefore(from) && !date.isAfter(to)

    companion object {
        fun validateThenCreate(
            from: LocalDate,
            to: LocalDate
        ): Either<NonEmptyList<DomainError>, PublicationPeriod> = either {
            zipOrAccumulate(
                { ensure(!to.isBefore(from)) {
                    DomainError("'to' date must not be before 'from' date")
                } }
            ) {
                PublicationPeriod(from, to)
            }
        }
    }
}
```

The value object does more than hold data. It owns behaviour related to that data, such as answering whether a date falls within the publication period.

## Put construction behind a domain boundary

`BlogPost` is a data class because value-based equality and a useful `toString()` are convenient. Its primary constructor is private, however. Callers must go through `validateThenCreate`:

```kotlin
@ConsistentCopyVisibility
data class BlogPost private constructor(
    val id: BlogPostId,
    val title: Title,
    val content: Content,
    val authorId: AuthorId
) {
    companion object {
        fun validateThenCreate(
            id: BlogPostId?,
            title: Title?,
            content: Content?,
            authorId: AuthorId?
        ): Either<NonEmptyList<DomainError>, BlogPost> = either {
            zipOrAccumulate(
                { ensureNotNull(id) {
                    DomainError("Blog Post ID must not be null")
                } },
                { validateTitle(title) },
                { validateContent(content) },
                { ensureNotNull(authorId) {
                    DomainError("Author ID must not be null")
                } }
            ) { validId, validTitle, validContent, validAuthorId ->
                BlogPost(validId, validTitle, validContent, validAuthorId)
            }
        }
    }
}
```

The nullable inputs are intentional. They model data arriving from an untrusted boundary: an HTTP request, message, persistence layer, or user interface. Once the factory succeeds, the resulting `BlogPost` has non-null fields and has passed its rules.

This is a useful DDD boundary. The outside world may contain incomplete or invalid data; the inside of the domain should contain objects that satisfy the domain's invariants.

## Context parameters keep validation focused

The validation helpers need a way to report a domain error. Arrow's `Raise<DomainError>` provides that capability. Rather than passing it explicitly through every function, the model uses a Kotlin context parameter:

```kotlin
context(raise: Raise<DomainError>)
private fun validateTitle(title: Title?): Title {
    raise.ensureNotNull(title) { DomainError("Title must not be blank") }
    raise.ensure(title.value.isNotBlank()) {
        DomainError("Title must not be blank")
    }
    return title
}
```

The function reads naturally: given a `Raise<DomainError>` context, validate this title and return it. The error channel is available to the function, but it does not become part of the ordinary argument list.

`ensure` and `ensureNotNull` short-circuit a single validation branch. At the aggregate level, `zipOrAccumulate` combines those branches so a caller receives all independent problems at once instead of fixing them one at a time.

That difference matters at an input boundary. Given four missing fields, the result is a `Left` containing four `DomainError` values. Given valid input, it is a `Right<BlogPost>`:

```kotlin
when (val result = BlogPost.validateThenCreate(id, title, content, authorId)) {
    is Either.Left -> result.value.all.forEach(::println)
    is Either.Right -> publish(result.value)
}
```

There is no exception to catch for an expected business validation failure, and the return type makes it difficult for a caller to ignore the possibility of failure accidentally.

## Why `@ConsistentCopyVisibility` matters

There is a subtle escape hatch when a validated type is a data class. Kotlin generates `copy()` for data classes. Historically, that generated method could remain public even when the primary constructor was private. A caller could then copy a valid post while replacing one property with an invalid value, bypassing the factory:

```kotlin
val invalid = validPost.copy(title = Title(""))
```

`@ConsistentCopyVisibility` opts this class into the newer behaviour: the generated `copy()` has the same visibility as the private constructor. Callers cannot use either the constructor or `copy()` as a back door around validation.

The trade-off is deliberate. To change a validated aggregate, expose a domain operation or construct a new instance through the validation boundary. Re-running the rules is more important than retaining a convenient unrestricted `copy()`.

Value classes also have practical boundaries: they can be boxed in some generic contexts and their JVM signatures are subject to name mangling, which can make Java interoperability less pleasant. That is usually a reasonable trade-off for a pure Kotlin domain module, but it is worth considering before exposing these types across a Java-facing API.

## Testing the domain model

Because the rules live in ordinary Kotlin classes, the tests are small and fast. There is no application context to start and no database to configure. The test suite checks the successful path, accumulation of missing-field errors, whitespace-only values, and the visibility of generated construction methods.

This style of testing is valuable beyond speed. The tests describe the model's language directly:

```kotlin
val result = BlogPost.validateThenCreate(
    id = BlogPostId("post-1"),
    title = Title("A useful post"),
    content = Content("Some content"),
    authorId = AuthorId("author-1")
)

val post = assertIs<Either.Right<BlogPost>>(result).value
```

The application layer can decide how to translate `DomainError` into an HTTP response, message rejection, or user-facing form error. The domain does not need to know which transport is involved.

## A small pattern with useful consequences

The sample is intentionally modest, but the design has a clear shape:

1. Use value classes and value objects to give domain concepts names and types.
2. Keep construction private when an object has invariants.
3. Validate untrusted input at the boundary.
4. Use `Either` to make expected failure explicit.
5. Accumulate independent validation errors for better feedback.
6. Keep domain rules framework-free and easy to test.

This is not about using functional programming vocabulary for its own sake. It is about making invalid states harder to represent, making failure visible, and giving each rule a natural home. Kotlin supplies the language features; Arrow supplies a concise error-handling model; DDD supplies the discipline for deciding what the rules mean.

----
As usual, full source code is available on GitHub: [demo-arrow-validation](https://github.com/hgbrown/demo-arrow-validation)
