Mastering CQRS Use Cases: A Structured Approach with Sealed Interfaces
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.

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:
- Action — fire-and-forget operations (e.g., logout, clear cache). No input or output beyond acknowledging the action completed.
- Query — read operations (e.g., list products). Takes no input (or only context) and returns data.
- Command — write operations (e.g., update profile). Accepts input and returns no meaningful output (success only).
- Exchange — data transformation operations (e.g., login). Both accepts input and returns output.
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.

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
- No more
UseCase<Unit, Unit>noise — The sealed interface eliminates generic boilerplate; each subclass explicitly defines its input/output contract. - Every use case follows the same structure — Whether it's an Action, Query, Command, or Exchange, the pattern remains consistent, making the codebase predictable and easy to navigate.
- Query or Command makes intent obvious — Naming a class
ListProducts : UseCase.Query<List<Product>>instantly communicates its purpose and side‑effect profile, aiding both readability and maintenance.
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.
Related Articles
- CME Group Announces Bitcoin Volatility Futures Launch: Key Questions Answered
- Docs.rs Streamlines Documentation Builds: Fewer Targets by Default
- How to Unpack the Major Evidence Revealed in the Musk v. Altman Trial
- How to Decipher Strategy's Bitcoin Acquisition and Tax-Loss Harvesting Playbook
- 10 Essential Concepts for Testing SaryPOS: A Flutter Widget & State Management Guide
- From Rigid Systems to Flexible Dialects: A Guide to Contextual Design Adaptation
- 6 Key Insights: How Bitcoin-Backed Loans Are Reshaping Homeownership for a New Generation
- Nvidia's CEO on China Market Loss and US AI Policy Backlash