WhatschatDocsFinance & Crypto
Related
TeraWulf's April 2026 Rally: Key Questions AnsweredBreaking: Amazon Slashes iPad A16 Prices to $299 in Limited-Time SaleArista Networks Stock Tumbles Despite Strong Q1 Earnings: What Investors Need to KnowCrypto Exchange Grinex Blames Western Intelligence for $15M Hack, Shuts Down OperationsHow to Analyze Apple’s Record R&D Spending as a Signal of AI InvestmentAI Coding Agents Gain Full Cloudflare Access: Zero-Touch Account Creation and Deployment Now Possible7 Reasons Why Stratum v2 Is Gaining Momentum in Bitcoin MiningRedesigning Enterprise AI: From Stateless Tools to Persistent Systems

Mastering CQRS Use Cases: A Structured Approach with Sealed Interfaces

Last updated: 2026-05-13 18:28:37 · Finance & Crypto

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:

  • 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.

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

  • 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.