Finance & Crypto

Mastering CQRS Use Cases: A Structured Approach with Sealed Interfaces

2026-05-13 18:28:37

Introduction

Command Query Responsibility Segregation (CQRS) is a powerful architectural pattern that separates read and write operations, leading to cleaner, more maintainable code. However, implementing use cases in a CQRS system can become messy without a consistent structure. In this article, we present a disciplined recipe for crafting use cases using sealed interfaces, making your intent explicit and your code robust. We'll explore the four fundamental use case types—Action, Query, Command, and Exchange—and three implementation strategies: Arrow with typed errors, the standard Result wrapper, and raw execution.

Mastering CQRS Use Cases: A Structured Approach with Sealed Interfaces
Source: dev.to

The Four Use Case Types

At the heart of our approach is a sealed interface UseCase<Input, Output> that defines a contract every use case must follow. Each operation type corresponds to a specific interaction pattern:

All four extend the same sealed interface, ensuring every use case adheres to a uniform shape and making polymorphism effortless:

sealed interface UseCase<Input, Output> {
    class Action : UseCase<Unit, Unit>
    class Query<Output> : UseCase<Unit, Output>
    class Command<Input> : UseCase<Input, Unit>
    class Exchange<Input, Output> : UseCase<Input, Output>
}

Three Implementation Strategies

You can implement the same use case in different ways depending on your project's error-handling philosophy and dependency tolerance. Here we demonstrate a GenerateSeed action using three popular approaches.

Arrow (Typed Errors) — The Chef's Choice

Using the Arrow library, you leverage its Raise context for typed error handling. This provides compile‑time guarantees about possible failures and integrates seamlessly with functional programming patterns.

class GenerateSeed(
    private val seedService: SeedService
) : UseCase.Action {
    override suspend fun Raise<Throwable>.action() =
        seedService.generateSeed().bind()
}

Result (Standard Wrapper) — Zero Dependencies

If you prefer to avoid external libraries, Kotlin's standard Result class (or a custom sealed hierarchy) works well. This approach is simple and dependency‑free, but errors are less explicit at the type level.

Mastering CQRS Use Cases: A Structured Approach with Sealed Interfaces
Source: dev.to
class GenerateSeed(private val service: SeedService) : UseCase.Action {
    override suspend fun action() = service.generateSeed().getOrThrow()
}

Raw (Direct Execution) — Zero Overhead

For maximum performance and minimal abstractions, execute the operation directly without any wrapper. This is suitable when errors are handled elsewhere (e.g., by an HTTP layer) or when the operation cannot fail.

class GenerateSeed(private val service: SeedService) : UseCase.Action {
    override suspend fun action() = service.generateSeed()
}

Why This Recipe Works

This approach scales from small projects to large enterprise systems. For a complete implementation example, check out the GitHub repository.

Conclusion

By adopting a sealed interface for your CQRS use cases, you gain a clear, self‑documenting structure that makes your architectural decisions explicit. The three implementation styles—Arrow, Result, and raw—allow you to choose the level of abstraction that fits your team and project constraints. Start cooking your use cases with this recipe today and enjoy cleaner, more maintainable code.

Explore

State Preschool Funding Hits Record Highs, but Quality Gaps Persist Across the Nation Securing VMware vSphere Against BRICKSTORM: Advanced Threat Mitigation for Virtualized Environments The Ancient Mystery of the Twisted-Jaw Creature: Tanyka amnicola Wildfire Smoke: A Growing Threat to Public Health 10 Key Insights from NASA's Record-Breaking Mars Helicopter Rotor Test