Mastering CQRS Use Cases: A Structured Approach with Sealed Interfaces

By

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.

Related Articles

Recommended

Discover More

Strengthening Python's Security: The PSRT's New Governance and MembershipAmazon Bedrock Guardrails Gets Cross-Account AI Safety Controls – Centralized Enforcement Now GAHow the Silver Fox Group Deploys the ABCDoor Backdoor via Phishing CampaignsHow to Safeguard Your Browser from Malicious AI Extensions That Steal Your Data10 Key Features of the iGame X870E Vulcan OC V14: Colorful's Overclocking Flagship