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
- 10 Essential Concepts for Testing SaryPOS: A Flutter Widget & State Management Guide
- Agent-Driven Cloud Deployment: How AI Can Now Fully Provision Cloudflare Accounts and Domains
- Revolutionary Hybrid Gadget Eliminates Traveler’s Two Biggest Annoyances: Dead Batteries and No Internet
- How to File an Emergency Motion to Vacate a Restraining Notice on Crypto Assets
- Reclaiming the American Dream: Why Sharing Our Prosperity Is the Path Forward
- Tokenization Drift: The Hidden Pitfall in LLM Prompts and How to Overcome It
- Navigating Polymarket: A Guide to Understanding Risks and Rewards in Decentralized Prediction Markets
- 10 Game-Changing Facts About Micron’s 245TB Data Center SSD