# Commanded Utils
[](https://hex.pm/packages/commanded_utils)
[](https://hexdocs.pm/commanded_utils)
[](LICENSE)
> Essential utilities and helpers for building CQRS/Event Sourcing applications with Commanded
**Commanded Utils** is a comprehensive toolkit that simplifies building event-sourced applications with [Commanded](https://github.com/commanded/commanded). It provides elegant DSLs, type-safe commands and events, automatic validation, enrichment pipelines, error handling, and more - with minimal boilerplate.
[δΈζζζ‘£](README_zh-CN.md)
## Features
- π¨ **Elegant DSL** - `defcommand` and `defevent` macros for clean, declarative definitions
- β
**Type Safety** - Built on Ecto schemas with automatic type casting and validation
- π **Enrichment Pipeline** - Clean, functional command enrichment with convention over configuration
- π¦ **Event Versioning** - Built-in upcasting support for seamless event evolution
- π‘οΈ **Error Handling** - Configurable retry logic and failure handling for event handlers
- π§° **Helper Functions** - 20+ utility functions for type conversions and decimal math
- π **Minimal Boilerplate** - No more verbose `defimpl` blocks - just define `enrich/1` functions
- π’ **Multi-tenancy Ready** - Optional tenant and creator fields support
## Installation
Add `commanded_utils` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:commanded, "~> 1.4"},
{:commanded_utils, "~> 0.1"}
]
end
```
Run `mix deps.get` to install.
## Quick Start
### Define a Command
```elixir
defmodule CreateUser do
use CommandedUtils.Command
@required_fields ~w(email)a
defcommand do
field :user_id, Ecto.UUID
field :email, :string
field :name, :string
field :age, :integer
end
# Optional: enrich before dispatch
def enrich(cmd) do
%{cmd | user_id: cmd.user_id || Ecto.UUID.generate()}
end
# Optional: custom validation
def changeset(struct, attrs) do
struct
|> super(attrs)
|> validate_format(:email, ~r/@/)
|> validate_number(:age, greater_than: 0)
end
end
```
### Define an Event
```elixir
defmodule UserCreated do
use CommandedUtils.Event
defevent do
field :user_id, Ecto.UUID
field :email, :string
field :name, :string
end
end
```
### Use Commands
```elixir
# Validate and create
{:ok, cmd} = CreateUser.new(%{
email: "john@example.com",
name: "John Doe",
age: 30
})
# Dispatch to aggregate
MyApp.Router.dispatch(cmd)
```
## Core Concepts
### Commands
Commands represent intentions to change state. They are validated before dispatch and can be enriched with additional data.
```elixir
defmodule PlaceOrder do
use CommandedUtils.Command
@required_fields ~w(customer_id items)a
defcommand do
field :order_id, Ecto.UUID
field :customer_id, Ecto.UUID
field :items, {:array, :map}
field :total_amount, :decimal
end
# Multi-step enrichment
def enrich(cmd) do
cmd
|> ensure_order_id()
|> calculate_total()
|> validate_inventory()
end
defp ensure_order_id(%{order_id: nil} = cmd) do
%{cmd | order_id: Ecto.UUID.generate()}
end
defp ensure_order_id(cmd), do: cmd
defp calculate_total(cmd) do
import CommandedUtils.Helpers
total =
cmd.items
|> Enum.map(&decimal_mult(&1.price, &1.quantity))
|> Enum.reduce(Decimal.new(0), &Decimal.add/2)
%{cmd | total_amount: total}
end
defp validate_inventory(cmd) do
# Return {:error, reason} to halt dispatch
# Return {:ok, cmd} or cmd to continue
if all_items_in_stock?(cmd.items) do
{:ok, cmd}
else
{:error, :insufficient_inventory}
end
end
end
```
### Events with Versioning
Events can evolve over time. Commanded Utils provides automatic upcasting:
```elixir
defmodule OrderPlaced do
use CommandedUtils.Event
# Current version
defevent version: 2 do
field :order_id, Ecto.UUID
field :customer_id, Ecto.UUID
field :items, {:array, :map}
field :payment_method, :string # Added in v2
end
# Upgrade v1 events to v2
def upcast(%{"version" => 1} = params, _metadata, 2) do
Map.put(params, "payment_method", "credit_card")
end
# No transformation needed for v1
def upcast(params, _metadata, 1), do: params
end
```
When old events are read from the event store, they're automatically upcasted to the latest version.
### Middleware Setup
Add Commanded Utils middleware to your router:
```elixir
defmodule MyApp.Router do
use Commanded.Commands.Router
# Add middleware
middleware CommandedUtils.Middleware.Logger
middleware CommandedUtils.Middleware.Enrich
# Define aggregates and dispatch
identify MyApp.UserAggregate, by: :user_id, prefix: "user-"
dispatch CreateUser, to: MyApp.UserAggregate
end
```
The `Enrich` middleware automatically calls `enrich/1` if defined in your command module.
### Projectors with Error Handling
```elixir
defmodule MyApp.UserProjector do
use Commanded.Projections.Ecto,
application: MyApp.CommandedApp,
repo: MyApp.Repo,
name: __MODULE__
# Automatic retry with configurable backoff
use CommandedUtils.EventHandlerFailureContext,
max_retries: 5,
retry_after: 1000,
skip: true
import CommandedUtils.Helpers
alias MyApp.User
project %UserCreated{} = evt, _meta, fn multi ->
user = %User{
id: evt.user_id,
email: to_string(evt.email),
name: to_string(evt.name),
age: to_integer(evt.age)
}
Ecto.Multi.insert(multi, :user, user)
end
project %UserUpdated{} = evt, meta, fn multi ->
set_fields = [
name: to_string(evt.name),
updated_at: meta.created_at
]
Ecto.Multi.update_all(
multi,
:user_updated,
user_query(evt.user_id),
set: set_fields
)
end
defp user_query(user_id) do
from(u in User, where: u.id == ^user_id)
end
end
```
## Helper Functions
Commanded Utils provides 30+ utility functions for common operations:
```elixir
import CommandedUtils.Helpers
# Type conversions
to_decimal("10.50") # #Decimal<10.50>
to_atom("active") # :active
to_integer("42") # 42
to_boolean("true") # true
to_string(:active) # "active"
# DateTime parsing (using Ecto.Type.cast)
{:ok, dt} = parse_utc_datetime("2024-01-01 12:00:00") # ~U[2024-01-01 12:00:00Z]
{:ok, date} = parse_date("2024-01-01") # ~D[2024-01-01]
{:ok, time} = parse_time("12:30:00") # ~T[12:30:00]
{:ok, ndt} = parse_naive_datetime("2024-01-01 12:00") # ~N[2024-01-01 12:00:00]
# DateTime parsing (raising on error)
parse_utc_datetime!("2024-01-01 12:00:00") # ~U[...] or raise
parse_date!("2024-01-01") # ~D[...] or raise
# Decimal operations
decimal_add(10, 5) # #Decimal<15>
decimal_sub(20, 5) # #Decimal<15>
decimal_mult(price, quantity) # Safe multiplication
decimal_div(total, count) # Safe division
# Decimal comparisons
decimal_gt_zero?(balance) # true/false
decimal_lt_zero?(balance) # true/false
decimal_eq_zero?(balance) # true/false
# UUID helpers
ensure_uuid(nil) # Generates new UUID
ensure_uuid("existing") # "existing"
```
All helpers handle `nil` values gracefully and provide safe type conversions.
### Usage in Projectors
```elixir
project %OrderCreated{} = evt, _meta, fn multi ->
# Parse dates using Ecto.Type.cast
{:ok, order_date} = parse_date(evt.order_date)
{:ok, due_date} = parse_date(evt.due_date)
{:ok, created_at} = parse_utc_datetime(evt.created_at)
order = %Order{
id: evt.order_id,
amount: to_decimal(evt.amount),
status: to_atom(evt.status),
order_date: order_date,
due_date: due_date,
created_at: created_at
}
Ecto.Multi.insert(multi, :order, order)
end
```
## Configuration
Configure optional fields globally:
```elixir
# config/config.exs
config :commanded_utils,
# Fields always included
required_fields: [:version],
# Optional fields (can be enabled per module)
optional_fields: [:tenant_id, :creator, :metadata]
```
Include optional fields in specific modules:
```elixir
defmodule CreateTenantUser do
use CommandedUtils.Command, include: [:tenant_id, :creator]
defcommand do
# tenant_id and creator automatically included
field :email, :string
end
end
```
### Aggregates
Define aggregates using the same type system:
```elixir
defmodule MyApp.ItemAggregate do
@required_fields []
use CommandedUtils.Type # Reuse the type system
import CommandedUtils.Helpers
alias Commanded.Aggregate.Multi
# Define aggregate state
deftype do
field :item_id, Ecto.UUID
field :name, :string
field :price, :decimal
field :stock, :integer, default: 0
field :deleted?, :boolean, default: false
end
# Command handlers
def execute(%__MODULE__{item_id: nil}, %CreateItem{} = cmd) do
%ItemCreated{
item_id: cmd.item_id,
name: cmd.name,
price: cmd.price
}
end
def execute(%__MODULE__{item_id: id} = state, %AdjustStock{item_id: id} = cmd) do
new_stock = state.stock + cmd.quantity
if new_stock < 0 do
{:error, :insufficient_stock}
else
%StockAdjusted{
item_id: id,
quantity: cmd.quantity,
new_stock: new_stock
}
end
end
# Return multiple events using Multi
def execute(%__MODULE__{item_id: id} = state, %UpdateItem{item_id: id} = cmd) do
state
|> Multi.new()
|> Multi.execute(fn _ ->
%ItemUpdated{
item_id: id,
name: cmd.name,
price: cmd.price
}
end)
|> Multi.execute(fn _ ->
# Automatically publish price change if price changed
if decimal_gt_zero?(decimal_sub(cmd.price, state.price)) do
%PriceIncreased{item_id: id, old_price: state.price, new_price: cmd.price}
else
[]
end
end)
end
# State mutators
def apply(%__MODULE__{} = state, %ItemCreated{} = evt) do
%__MODULE__{
state
| item_id: evt.item_id,
name: evt.name,
price: to_decimal(evt.price)
}
end
def apply(%__MODULE__{} = state, %StockAdjusted{} = evt) do
%__MODULE__{state | stock: evt.new_stock}
end
def apply(%__MODULE__{} = state, %ItemUpdated{} = evt) do
%__MODULE__{
state
| name: evt.name,
price: to_decimal(evt.price)
}
end
# Lifecycle management
def after_event(%ItemDeleted{}), do: :stop
def after_event(_), do: :timer.hours(1)
end
```
**Key Features:**
- β
Reuse `CommandedUtils.Type` for aggregate state
- β
Use `Helpers` for safe type conversions
- β
Return multiple events with `Commanded.Aggregate.Multi`
- β
Business logic validation in `execute/2`
- β
Aggregate lifecycle management
## Examples
See the [Bank Account Example](lib/commanded_utils/examples/bank_account.ex) for a complete working implementation including:
- Commands with validation and enrichment
- Events with versioning
- Aggregate implementation with Multi
- Business logic and state management
- Lifecycle management
## Documentation
- [API Documentation](https://hexdocs.pm/commanded_utils)
- [Getting Started Guide](guides/getting-started.md)
- [Commands and Events Guide](guides/commands-and-events.md)
## Comparison with Plain Commanded
### Before (Plain Commanded)
```elixir
# Verbose defimpl for enrichment
defmodule CreateUser do
@derive Jason.Encoder
defstruct [:user_id, :email, :name]
defimpl Commanded.Middleware.Enrichable, for: __MODULE__ do
def enrich(%CreateUser{} = cmd, _) do
# 40+ lines of boilerplate...
end
end
end
```
### After (With Commanded Utils)
```elixir
# Clean and focused
defmodule CreateUser do
use CommandedUtils.Command
@required_fields ~w(email)a
defcommand do
field :user_id, Ecto.UUID
field :email, :string
field :name, :string
end
def enrich(cmd) do
%{cmd | user_id: cmd.user_id || Ecto.UUID.generate()}
end
end
```
**Reduction: ~70% less boilerplate code**
## Testing
```bash
# Run all tests
mix test
# Run with coverage
mix test --cover
# Run specific test file
mix test test/commanded_utils/command_test.exs
```
## Contributing
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Credits
Built with β€οΈ by [zven21](https://github.com/zven21)
Powered by [Commanded](https://github.com/commanded/commanded)
## Related Projects
- [Commanded](https://github.com/commanded/commanded) - CQRS/ES framework for Elixir
- [EventStore](https://github.com/commanded/eventstore) - Event store for Commanded
- [Commanded Ecto Projections](https://github.com/commanded/commanded-ecto-projections) - Ecto projections for Commanded
## Support
- π [Documentation](https://hexdocs.pm/commanded_utils)
- π¬ [GitHub Issues](https://github.com/zven21/commanded_utils/issues)
- π [Bug Reports](https://github.com/zven21/commanded_utils/issues/new)
---
**Star β this repository if you find it helpful!**