The Application Layer (Use Cases)

March 18, 2026 ยท View on GitHub

The application layer orchestrates domain logic by composing port calls into meaningful use cases. It is split into commands (writes) and queries (reads).

The Executor Pattern

Define a generic executor interface so all use cases share a common shape:

// internal/app/executors/executor.go

type Executor[Params any] interface {
    Execute(ctx context.Context, params Params) error
}

type ExecutorWithReturn[Params, Result any] interface {
    Execute(ctx context.Context, params Params) (Result, error)
}

Commands (Write Operations)

// internal/app/commands/create_order.go

type CreateOrderCommand struct {
    repo              repository.OrderRepository
    notificationSvc   notification_service.NotificationService
}

// Input DTO for this command
type CreateOrder struct {
    CustomerID domain.ID
    Items      []CreateOrderItem
}

func (c *CreateOrderCommand) Execute(ctx context.Context, params CreateOrder) (domain.ID, error) {
    // 1. Validate input
    if err := params.validate(); err != nil {
        return "", err
    }

    // 2. Execute business logic (delegates to repository port)
    id, err := c.repo.Create(ctx, repository.CreateOrderParams{
        CustomerID: params.CustomerID,
        Items:      mapItems(params.Items),
    })
    if err != nil {
        return "", err
    }

    // 3. Side effects (delegates to notification port)
    _ = c.notificationSvc.SendOrderConfirmation(ctx, ...)

    return id, nil
}

Queries (Read Operations)

// internal/app/queries/get_order.go

type GetOrderQuery struct {
    repo repository.OrderRepository
}

type GetOrder struct {
    OrderID domain.ID
}

type GetOrderResult struct {
    ID     domain.ID
    Status domain.OrderStatus
    Items  []OrderItem
}

func (q *GetOrderQuery) Execute(ctx context.Context, params GetOrder) (GetOrderResult, error) {
    result, err := q.repo.GetByID(ctx, repository.GetByIDParams{ID: params.OrderID})
    if err != nil {
        return GetOrderResult{}, err
    }
    return mapToGetOrderResult(result), nil
}

Application Struct

The application struct groups all commands and queries into a single object that handlers receive:

// internal/app/admin_app.go

type AdminApp struct {
    Commands AdminCommands
    Queries  AdminQueries
}

type AdminCommands struct {
    CreateOrder ExecutorWithReturn[CreateOrder, domain.ID]
    DeleteOrder Executor[DeleteOrder]
}

type AdminQueries struct {
    GetOrder   ExecutorWithReturn[GetOrder, GetOrderResult]
    ListOrders ExecutorWithReturn[ListOrders, ListOrdersResult]
}

Rules

  • Commands and queries depend only on port interfaces, never on concrete adapters.
  • Each command/query has its own input DTO with a validate() method.
  • The application layer does not know about HTTP, gRPC, or any transport mechanism.