Elixir is a dynamic, functional language designed for building scalable and maintainable applications.
It leverages the Erlang VM, known for running low-latency, distributed and fault-tolerant systems.
First appeared in 2011 — six years old.
Latest major release – 1.4.0 – in January 2017.
Elixir's key features
Shared nothing concurrent programming via message passing.
Immutable state.
Emphasis on recursion and higher-order functions instead of side-effect-based looping.
Pattern matching.
Macros and meta-programming.
Polymorphism via a mechanism called protocols.
"Let it crash"
Elixir takes full advantage of OTP
Elixir can call Erlang code, and vice versa, without any conversion cost at all.
Elixir can use Erlang libraries.
OTP behaviours:
GenServer
Supervisor
Application
Behaviours provide a module of common code — the wiring and plumbing — and a list of callbacks you must implement for customisation.
Elixir's promise
Elixir does not promise:
That your code will scale horizontally.
Fault tolerant systems with no effort.
But promises to give you:
Tools that you can employ to make it happen.
Patterns and building blocks of reliability instead.
Elixir's concurrency model
Elixir code runs inside lightweight threads of execution, called processes.
Processes are also used to hold state.
A process in Elixir is not the same as an operating system process. Instead, it is extremely lightweight in terms of memory and CPU usage.
An Elixir application may have tens, or even hundreds of thousands of processes running concurrently on the same machine.
A single Elixir process is analogous to the event loop in JavaScript.
Actor model
An Actor has a mailbox.
Actors communicate with other Actors by sending them immutable messages.
Messages are put into the Actor’s mailbox.
When an Actor’s mailbox has a message, code is run with that message as an argument. This code is called serially.
When an Actor encounters an error, it dies.
Message passing
Processes are isolated and exchange information via messages:
current_process = self()
# spawn a process to send a message
spawn_link(fn ->
send(current_process, {:message, "hello world"})
end)
# block current process until the message is received
receive do
{:message, message} -> IO.puts(message)
end
# ... or flush all messages
flush()
Processes encapsulate state
State is held by a process while it runs an infinite receive loop:
defmodule Counter do
def start(initial_count) do
loop(initial_count)
end
defp loop(count) do
new_count = receive do
:increment -> count + 1
:decrement -> count - 1
end
IO.puts(new_count)
loop(new_count)
end
end
A supervisor is responsible for starting, stopping, and monitoring its child processes.
The basic idea of a supervisor is that it is to keep its child processes alive by restarting them when necessary.
Processes can supervise other processes:
If a supervised process dies, the supervisor is sent a message.
If this message isn’t handled, the supervisor dies.
Supervision is key to Erlang – and Elixir's – "let it crash" philosophy.
Example Supervisor
Define child processes to monitor and restart:
import Supervisor.Spec
children = [
worker(ExampleServer, [], [name: ExampleServer])
]
A supervisor's children can also include other supervisors.
Elixir’s built-in unit testing framework, ExUnit, takes advantage of macros to provide great error messages when test assertions fail.
defmodule ListTest do
use ExUnit.Case, async: true
test "can compare two lists" do
assert [1, 2, 3] == [1, 3]
end
end
The async: true option allows tests to run in parallel, using as many CPU cores as possible.
Unit test execution
Running the failing test produces a descriptive error:
$ mix test
1) test can compare two lists (ListTest)
test/list_test.exs:13
Assertion with == failed
code: [1, 2, 3] == [1, 3]
left: [1, 2, 3]
right: [1, 3]
The equality comparison failed due to differing left and right hand side values.
Using mix to create an Elixir app
$ mix new example --sup --module Example --app example
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/example.ex
* creating lib/example/application.ex
* creating test
* creating test/test_helper.exs
* creating test/example_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd example
mix test
Run "mix help" for more commands.
Let's have a peek inside an Elixir app …
Environment specific configuration: config.exs.
Application supervisor defined, by using the --sup flag to mix.
A productive web framework that does not compromise speed and maintainability.
Written in Elixir.
Implements the server-side model-view-controller (MVC) pattern.
Channels for soft-realtime features.
Pre-compiled view templates for blazing speed.
Uses the Erlang HTTP server, Cowboy.
Response times measured in microseconds (μs).
Phoenix vs another web framework
Comparison of technologies used in two real-life web servers.
Command Query Responsibility Segregation and event sourcing
CQRS/ES
At it’s simplest CQRS is the separation of commands from queries.
Commands are used to mutate state in a write model.
Queries are used to retrieve a value from a read model.
The read and write models are different logical models.
They may also be separated physically by using a different database or storage mechanism.
Commands
Commands are used to instruct an application to do something.
They are named in the imperative:
Register account
Transfer funds
Mark fraudulent activity
Domain events
Domain events indicate something of importance has occurred within a domain model.
They are named in the past tense:
Account registered
Funds transferred
Fraudulent activity detected
Queries
Domain events from the write model are used to build the read model.
The read model is optimised for querying, using whatever technology is most appropriate:
Relational database
In-memory store
NoSQL database
Full-text search index
Event sourcing
Application state changes are modelled as a stream of domain events:
An aggregate's current state is built by replaying its domain events:
f(state, event) => state
Event streams
Domain events are persisted in order – as a logical stream – for each aggregate.
The event stream is the canonical source of truth.
It is a perfect audit log.
All other state in the system may be rebuilt from these events:
Read models are projections of the event stream.
You can rebuild the read model by replaying every event from the beginning of time.
Why choose CQRS/ES?
Domain events describe your system activity over time using a rich, domain-specific language.
They are an immutable source of truth for the system.
Auditing.
They support temporal queries, and after the fact data analysis of events.
A separate logical read model allows optimised and highly specialised query models to be built.
Bypass the object-relational (ORM) impedance mismatch.
Benefits of using CQRS
The processing of commands and queries is asymmetrical, you can:
Scale the read and write models independently.
Dedicate the appropriate number of servers to each side.
Events and their schema provide the ideal integration point for other systems.
They allow migration of read-only data between persistence technologies by replaying and projecting all events.
Costs of using CQRS
Events also provide a history of your poor design decisions.
Events are immutable.
It's an alternative – and less common – approach to building applications than basic CRUD.
It demands a richer understanding of the domain being modelled.
CQRS adds risky complexity.
Eventual consistency.
Recipe for building a CQRS/ES application in Elixir
A domain model containing our aggregates, commands, and events.
Hosting of an aggregate root and a way to send it commands.
An event store to persist the domain events.
Read model store for querying.
Event handlers to build and update the read model.
A web front-end UI to display read model query data, and to dispatch commands to the write model.
High-level application lifecycle
Read model queried to display data: UI, API.
Command built from a user request: Form POST data, API.
Command is validated and authorised.
Aggregate is located: its state rebuilt from previous events, or created from scratch.
Command passed to aggregate, business logic is validated.
Domain events are appended to the aggregate's event stream.
Its internal state is updated by applying these events.
Event handlers are notified of the events: read-model updated in response.
An aggregate in domain-driven design
Defines a consistency boundary for transactions and concurrency.
Aggregates should also be viewed from the perspective of being a "conceptual whole".
Are used to enforce invariants in a domain model.
Naturally fit within Elixir's actor concurrency model:
GenServer enforces serialised access.
Communicate by sending messages: commands and events.
An event-sourced aggregate
Must adhere to these rules:
Each public function must accept a command and return any resultant domain events, or raise an error.
Its internal state may only be modified by applying a domain event to its current state.
Its internal state can be rebuilt from an initial empty state by replaying all domain events in the order they were raised.
Let's build an aggregate in Elixir
defmodule ExampleAggregate do
# aggregate's state
defstruct [
uuid: nil,
name: nil,
]
# public command API
def create(%ExampleAggregate{}, uuid, name) do
%CreatedEvent{
uuid: uuid,
name: name,
}
end
# state mutator
def apply(%ExampleAggregate{} = aggregate, %CreatedEvent{uuid: uuid, name: name}) do
%ExampleAggregate{aggregate |
uuid: uuid,
name: name,
}
end
end
A bank account example
This example provides three public API functions:
To open an account: open_account/2.
To deposit money: deposit/2.
To withdraw money: withdraw/2.
A guard clause is used to prevent the account from being opened with an invalid initial balance.
This protects the aggregate from violating the business rule that an account must be opened with a positive balance.
Using the aggregate root
Initial empty account state:
account = %BankAccount{}
Opening the account returns an account opened event:
I've shown examples of aggregates implemented using pure functions.
The function always evaluates the same result value given the same argument value.
A pure function is highly testable.
You will focus on behaviour rather than state.
Decouple your domain from the framework's domain.
Build your application separately first, and layer the external interface on top.
Unit testing an aggregate
defmodule BankAccountTest do
use ExUnit.Case, async: true
alias BankAccount.Commands.OpenAccount
alias BankAccount.Events.BankAccountOpened
describe "opening an account with a valid initial balance"
test "should be opened" do
account = %BankAccount{}
open_account = %OpenAccount{
account_number: "ACC123",
initial_balance: 100,
}
account_opened = BankAccount.open_account(account, open_account)
assert account_opened == %BankAccountOpened{
account_number: "ACC123",
initial_balance: 100,
}
end
end
end
Unit testing a CQRS/ES application
Create factory functions for commands and events.
Use these to direct and verify aggregate behaviour.
Events can also be used to test: event handlers; read-model projections; inter-aggregate communication using process managers.
defmodule Factory do
use ExMachina
def open_account_factory do
%BankAccount.Commands.OpenAccount{
account_number: "ACC123",
initial_balance: 100,
}
end
end
Hosting an aggregate in a GenServer
One Elixir GenServer process per aggregate instance:
Serialises access to an aggregate instance.
Built-in support for terminating idle processes.
Registry used to track active aggregates:
Maps an aggregate identity to a process id (PID).
Handles side effects from pure aggregate functions.
Commands are routed to an instance.
Executing a command
Rebuild an aggregate's state from its events.
Execute the aggregate function, providing the state and command.
Update the aggregate state by applying the returned event(s).
Append the events to storage.
An error will terminate the process:
Caller will receive an {:error, reason} tagged tuple.
Aggregate state rebuilt from events in storage on next command.
Command dispatch
Synchronous command dispatch:
Receiving an :ok response indicates the command succeeded.
Middleware can be used to validate, audit commands.
Commands routed to a process hosting an aggregate instance:
defmodule BankRouter do
use Commanded.Commands.Router
dispatch OpenAccount,
to: OpenAccountHandler,
aggregate: BankAccount,
identity: :account_number
end
:ok = BankRouter.dispatch(%OpenAccount{account_number: "ACC123", initial_balance: 1_000})
Process managers
A process manager is responsible for coordinating one or more aggregates.
It handles events and may dispatch commands in response.
Each process manager has state used to track which aggregate roots are being orchestrated.
They are vital for inter-aggregate communication, coordination, and long-running business processes.
Typically, you would use a process manager to route messages between aggregates within a bounded context.
Before I forget …
It is worth remembering that domain events are the contracts of our domain model.
They are recorded within the immutable event stream of the aggregate.
A recorded domain event cannot be changed; history cannot be altered.
I'll show you how to migrate, modify, and retire domain events — in effect rewriting history — later.
An Elixir CQRS/ES case study
Let's explore a real-world example of implementing these concepts in a Phoenix-based web app.
Strava
Strava is a very popular social network for athletes (cyclists and runners).
Who record their rides and runs, and upload them to Strava.
Strava users create segments from sections of their routes.
Athletes can compare themselves against other Strava users who cycle or run along the same routes.
Segment Challenge
Segment Challenge allows an athlete to create a competition for a cycling club and its members.
A different Strava segment is selected each month to compete on.
Club members' attempts at each segment are fetched from Strava's API.
Their efforts are ranked by time on a leaderboard.
Replaces manual tracking of each athlete's segment efforts in a spreadsheet.
The site is entirely self-service: any Strava member can host a challenge for their own cycling club.
Journey to CQRS/ES in Elixir
Segment Challenge began life as a vanilla Phoenix web application.
Fell into trap of database schema == domain model.
I wanted to add an activity feed …
Seems like a good fit for an event sourced system.
So I stopped building the site and:
Built an event store in Elixir using PostgreSQL: eventstore
9 months later, added activity feed using a read-model projection.
Building an event store
Uses PostgreSQL for persistence.
Only requires four tables:
events
snapshots
streams
subscriptions
Events are serialized to JSON, but stored as binary data.
Subscriptions use a hybrid push/pull notification model.
In-memory pub/sub; read from storage on catch-up or new subscriber.
Event store API
defmodule EventStore do
@doc """
Append one or more events to a stream atomically.
"""
def append_to_stream(stream_uuid, expected_version, events)
@doc """
Reads the requested number of events from the given stream,
in the order in which they were originally written.
"""
def read_stream_forward(stream_uuid, start_version \\ 0, count \\ 1_000)
@doc """
Subscriber will be notified of each batch of events persisted to a single stream.
"""
def subscribe_to_stream(stream_uuid, subscription_name, subscriber, start_from \\ :origin)
@doc """
Subscriber will be notified of every event persisted to any stream.
"""
def subscribe_to_all_streams(subscription_name, subscriber, start_from \\ :origin)
end
Event store single writer
A single Elixir process is used to append events to the database.
Assigns incrementing identifier to each persisted event.
Guarantee consistent ordering of events within the event store.
Architecting an Elixir application
An OTP application is one or more modules that implement a specific piece of functionality.
Analogous to a microservice.
Elixir provides support for internal dependencies specific to a project by creating an umbrella project.
Umbrella projects allow you to create one project that hosts many OTP applications while keeping all of them in a single source code repository.
Elixir umbrella application
authorisation - Policies to authorise command dispatch.
challenges - Core domain model, command router, process managers, read model projections, queries, and periodic tasks.
commands - Modules for each command.
events - Modules for each domain event.
infrastructure - Serialization and command middleware.
projections - Ecto repository and database migrations to build the read model database schema.
web - Phoenix web front-end.
Let's take a look at the implementation
Aggregate root: Challenge
Commands and events.
Unit and integration testing.
Routing commands: Router
Event handling: StageEventHandler.
Process manager: ChallengeCompetitorProcessManager
Read model:
Projections.
Querying.
Challenge aggregate root
Public command functions:
Accept challenge state and a command.
Return zero, one, or many domain events in response.
Aggregate protects itself against commands that would cause an invariant to be broken.
Pattern matching is used to validate the state of the aggregate.
Every domain event returned by the aggregate has a corresponding apply/2 function to mutate its state.
Commands & events
Defining a command with validation: CreateChallenge
An Event: ChallengeCreated
Decoding an event struct from JSON using a protocol implementation.
Used when custom deserialisation is required.
Unit & integration testing
Unit testing an aggregate: ChallengeTest
Integration testing a use case: HostChallengeTest
Tag individual tests to allow specific test runs:
mix test --only unit
mix test --only integration
mix test --only wip
Use mix test.watch to run tests each time you save a file:
mix test.watch --only " wip"
Stage event handler
Used as a background worker.
Fetches data from the Strava API.
Dispatches command to an aggregate with retrieved data.
Supervised to handle restarting on failure.
Challenge competitor process manager
Used to track atheletes who are competiting in a challenge.
Records which challenges are being hosted by a club.
Adds, or removes, members when they join or leave a club.
Events are routed to an instance of the process manager using the interested?/1 function.
Read model
Projection: ChallengeProjection
Uses Ecto, a database wrapper and query language for Elixir.