DEV Community

Cover image for A recipe for the Perfect Use Case
numq
numq

Posted on

A recipe for the Perfect Use Case

Today we’re cooking the perfect Use Case — and CQRS will be our recipe.

Ingredients

  • Action — fire and forget (logout, clear cache)
  • Query — read data (list products)
  • Command — write data (update profile)
  • Exchange — transform data (login)

All four types extend a sealed interface:

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>
}
Enter fullscreen mode Exit fullscreen mode

Usecases

Three Ways to Season It

Arrow (Typed Errors) — the chef’s choice:

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

Result (Standard Wrapper) — zero dependencies:

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

Raw (Direct Execution) — zero overhead:

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

Why This Recipe Works

  • No more UseCase<Unit, Unit> noise
  • Every use case follows the same structure
  • Query or Command makes intent obvious

Full implementation in the GitHub repository.

Top comments (0)