---
title: CQRS (Command/Query Separation)
type: concept
created: 2026-09-06
updated: 2026-09-06
sources: [codebase snapshot 2026-09-06]
tags: [cqrs, commands, queries, application-layer]
---

# CQRS (Command/Query Separation)

Project-Board uses a lightweight, **method-level** CQRS split in the Application layer: write operations live under `Commands/`, read operations under `Queries/`. Each is a single **invokable class** — the handler and the use case are the same object, with the core logic in `__invoke()`.

> **Note on provenance:** reflects the code as of 2026-09-06; no written ADR exists. This is _partial_ CQRS — there is no separate write/read model, no event sourcing, no separate query database.

## Layout convention

```
src/Application/Commands/{Context}/{Action}Command/{Action}.php
src/Application/Queries/{Context}/{Action}Query/{Action}.php
```

The folder carries the `Command`/`Query` suffix; the file and class name is just the action (no stutter). The class _is_ the handler.

## Current state

The template's original `Order` command/query pair was removed on 2026-09-06 (see [log](../log.md)). The **only** application use case currently in the codebase is the `User` login command:

- `src/Application/Commands/User/CreateSessionCommand/CreateSession.php` → class `CreateSession`, invokable.

There are **no queries** yet — the `Queries/` directory is currently empty, so the read side of the split is aspirational until the tracking contexts get their own queries.

## Example: the `CreateSession` command

`CreateSession` (`src/Application/Commands/User/CreateSessionCommand/CreateSession.php`):

- Injects a user repository.
- `__invoke(string $email, string $password): array` — looks up the user, checks the password, issues a Sanctum token, returns a raw array.

> **Flag:** this command **leaks into Infrastructure** (it imports `App\Models\User`, `Hash`, and a concrete repository) and returns a raw array rather than a DTO. See [user](../entities/user.md).

## Transport objects

Commands/Queries are meant to return **DTOs** (`src/Application/DTOs/`) rather than domain aggregates, so the presentation layer never sees domain internals. `SessionDto` exists but is currently **unused** — the `CreateSession` command returns a raw array (see [user](../entities/user.md)).

## Controllers as thin adapters

Controllers build the command/query from the request and invoke it. Example — `app/Http/Controllers/Api/V1/Auth/CreateSessionController.php`:

```php
$dto = $createSession(
    $request->validated('email'),
    $request->validated('password'),
);
return AuthResource::make($dto);
```

A web controller would mirror this but render an Inertia page. See [inertia-react-integration](inertia-react-integration.md).

## Observations / open questions

- This is **in-process**, synchronous CQRS (no command bus, no messaging).
- The command/query naming (folder-suffix + bare handler class) is a deliberate convention — see [custom-generators](custom-generators.md), which scaffolds these exact shapes.
- Whether the split should grow into full CQRS (separate read models) is an open question worth an ADR later.

## Related pages

- [ddd-layering](ddd-layering.md) — where the Application layer sits
- [domain-events](domain-events.md) — side effects from commands
- [repository-mapper-pattern](repository-mapper-pattern.md) — how commands/queries persist