Live Tea — Making a CQRS/DDD/Liveview/Elm-architecture chat app

Aksel Stadler Kjetså
8 min readMar 24, 2021

I considered adding AI to the title to buzz even more, but I refrained.

According to https://mcfunley.com/choose-boring-technology you have a quota on how many new things you can try at once in a project. This demo has as much respect for quotas as a Russian fishing vessel, and has nothing but non-boring tech. Naturally, this can make the point of this blag a bit hard to grasp, so to be precise.

My main goal is to:

  • Show how phoenix liveview (LV) and CQRS can work together to make fast (really fast), and robust applications.
  • Inspire people to see the possibilities of “server-side-rendering”[¹] with websockets

Super opinionated stuff that I wanted to test, but are not essential. They might be good design choices, but also... they might not:

When I write actor, I mean an elixir process, which is not the same as a UNIX process (more lightweight). You could easily run millions of actors on your computer.

What I have made

A chat room web app using CQRS and no custom JavaScript

How it works

  1. A browser goes to livechat.stadler.no/some_id and gets html and JS
  2. A WS is set up between client and the backend (handled by LV)
  3. Liveview spawns an actor with the viewed html as its state (handled by LV)
  4. The actor subscribes to to “chat:some_id”
  5. A SendMessage Command is sent via WS, dispatched to an aggregate.
  6. MessageSent event is written to EventStore
  7. An actor subscribing go ES gets the MessageSent event and applies it to the chat state
  8. The updated chat is broadcasted
  9. The html is updated and the diff is sent over WS
  10. On the client morphdom applies this html diff and the user sees the new message. (handled by LV)

DDD (Domain driven design)

Domain driven design is about having the code reflect the language you use at work.

The idea is that developers and product/sale people can speak the same language. #hallelujah.

This, naturally, won’t work. Tribe language has been around forever, and DDD won’t change that. But, admittedly, its easier to say SendMessage and refer to the message sending function, than the MessageSendingFactoryClassInterfaceImpl.

When doing DDD get a bunch of domain experts in a room and define all the business events. In the chat app this is just the MessageSent event. Also, discover domain limitations on when this event can occur. E.g. Has the user sent too many messages the last minute? Is it a duplicate event? etc.. In the chat app I just let everything through.

CQRS

CQRS is an exceptionally forgettable abbreviation, with the even more bland long name (Command Query Response Segregation).

It’s an architecture pattern were you can use different models for your writes than your reads.

As in any stateful application you use a model to validate whether an update (write) can take place. With CQRS, however, you can present a different one on reads. This might seem overly complex (and it often is). On the pro side it gives a high degree of freedom in changing the data you present. If curious, read more here https://martinfowler.com/bliki/CQRS.html.

CQRS often is combined with event sourcing. That is, your system state is derived from a series of immutable events. This is naturally what I set out to do with my over-engineered one-event webapp.

So without further ado we define the event types:

lib/events.ex

defmodule MessageSent do 
@derive Jason.Encoder
defstruct [:sender, :chat_id, :content]
end

Then do some linguistic flexing and create corresponding commands

lib/commands.ex

defmodule SendMessage do 
defstruct [:sender, :chat_id, :content]
end

Now, the domain limitations need to be validated. In CQRS terminology this is done by the aggregate. The aggregate reads all the events in a stream and builds up a state. Based on this state the command is either rejected or an event is created. Which events should belong to the same stream is in this case rather intuitive. All messages in a single chat are in the same stream.

For most CQRS applications all events are read from scratch on each command attempt. In elixir, for reasons I will come back to, in-memory state is trivial and safe, thus the state can just reside in an actor.

In this example I use a CQRS package called commanded that does this heavy lifting for us. So we can just focus on the logic.

lib/aggregates.ex

defmodule Chat do 
defstruct [chat_id: nil, messages: []]
def execute(%Chat{}, %SendMessage{...}) do
%MessageSent{sender: sender, chat_id: chat_id, content: content} end
def apply(%Chat{} = chat, %MessageSent{} = event) do
%Chat{chat | messages: [event | chat.messages] |> Enum.reverse()}
end
end

As mentioned, CQRS is about splitting the “write-model” from the “read-model”. Now we’re done with the write side. On the read side we set up event-handlers. These are either one offs, like sending an SMS, or something that can be replayed, like building up a Read Model.

Here, we only have one event handler. All it does is building up the states of each chatroom. That just means to append the messages in a list.

The Read Model state is usually written to a SQL db, but I use Mnesia. The reason is mostly because I like fast things.

  • It has RAM-copies of state so lookups are fast
  • It’s built into Elixir/Erlang, and runs on the BEAM

After a successful write, I broadcast the new state. As pub-subs are integrated in the Elixir runtime this is rather simple.

lib/event_handlers.ex

defmodule ChatReadModel do  defmodule Model do
use Memento.Table, attributes: [:chat_id, :sender,:content],
type: :bag
end
use Commanded.Event.Handler,
application: LiveTea.App,
name: __MODULE__
def handle(%MessageSent{} = event, _metadata) do
Memento.transaction! fn ->
Memento.Query.write(%Model{..})
end
Phoenix.PubSub.broadcast(LiveTea.PubSub,
"chat:"<> event.chat_id,
get(event.chat_id))
end
def get(chat_id) do
Memento.transaction! fn ->
Memento.Query.select(Model, {:==, :chat_id, chat_id})
end
end
end

Phoenix liveview & Elixir

Now, to the bread and butter of this post. This is what I actually want to show.

To get all the terminology straight we’ll do as Erlend Loe and make a list

  • Erlang, a language made by Ericsson in the 90s to handle telecommunications switches.
  • BEAM, the runtime Erlang runs on.
  • Elixir, a language created in 2011, by Jose Valim, that runs on the BEAM. The syntax is very similar to Ruby making it attractive to a greater (as in larger) community.
  • Phoenix, the most popular web framework for Elixir
  • Phoenix Liveview, an addition to Phoenix released in v.1.5 that allows for server-initiated re-render of web pages.

Read more here: https://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript

Phoenix liveview enables “server-side-rendered”[¹] single-page web pages . It does this by the power of a brand-new (not really) technology called websockets. Liveview sets up a websocket connection between the client and the server. Commands goes from the client, and an updated html is sent back. Morphdom then seamlessly updates the view.

Why isn’t this implemented in other backend languages? Well, it’s not for lack of trying.. However websockets are stateful, and most languages have treated state as guy coughing in 2020.

In Elixir/Erlang state is trivial. It was made to handle telecom switches at Ericsson. In fact it’s an (unintentional) implementation of the actor model. Each websocket is an isolated process (actor) that can only communicate with other actors through message passing. I.e. Share by communicating, don’t communicate by sharing (I’m looking at you C++).

My chat app (like any other elixir app) starts inn application.ex

def start(_type, _args) do
# Start the Telemetry supervisor
LiveTeaWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: LiveTea.PubSub},
# Start the Endpoint (http/https)
LiveTeaWeb.Endpoint,
LiveTea.App,
ChatReadModel
end

Each line in the function spawns an actor. For example the ChatReadmodel mentioned before is spawned here. It starts listening for events and handles these as they come. The phoneix framework starts in the LiveTeaWeb.Endpoint, which is defined in lib/live_chat_web/endpoint.ex

This actor listens to http requests and spawns an actor for each, making all requests concurrent. The Endpoint applications handles setting up WS connections and other miscs, usually the file does not have to be edited from what phoenix gives when generating a new app.

At the end of the endpoint.ex the router is called. This file must be edited. For the chat app it looks like this

...
scope "/", LiveTeaWeb do
pipe_through :browser

# Pokemon route (gotta catch em all)
live "/*path", PageLive
end
...

This triggers the liveview called PageLive. This is the actor that holds the clients state and communicates with it via WS,

A liveview must contain at least two functions. A mount(params, sessions, socket) and a render(assigns). mount() is called by the router to spawn the actor and set up the liveview, and the render function returns the HTML.

Whenever the socket is updated, the render function is called anew, and html diffs are sent over the wire.

Usually, the routing is handled by.. well, the router. But as mentioned before I followed the elm architecture, so I just chucked all that logic into the PageLive module. The reason is that when I first started with MVC I got confused by all the different files, so I spent much time refactoring. TEA was a sweet relief as I could just focus on the logic and as long as I wrote pure functions I could move them later. But tbh. I might have been drinking too much of the kool-aid

The code with comments below show how the LiveVew actor is spawned and how the initial render takes place.

@impl true
## Mounts the liveview and assigns the initial socket state
def mount(_params, session, socket) do
case session["name"] do
nil -> {:ok, socket}
x -> {:ok, assign(socket, name: x)}
end
end# This function is called on the initial render,
# and everytime the url is updated.
# Also when it's just mutating the window. parameter.
# This is my router.
def handle_params(%{"path" => path}, _uri, socket) do
case path do
# Set page to home page
[] -> {:noreply, assign(socket, page: :home)}

# Subscribe to the chat, read the current chat state, and set the page to
# the chat_id
[chat_id] -> Phoenix.PubSub.subscribe(LiveTea.PubSub, "chat:"<>chat_id)
messages = ChatReadModel.get(chat_id)
{:noreply, assign(socket, page: :chat, chat_id: chat_id, messages: messages)}
end

# Render html
def render(assigns) do
~L"""
<%= case @page do %>
<%=:chat -> %> <%= chat_page(assigns)%>
<%=_-> %> <%= home_page(assigns)%>
<% end %>

"""
end

Along with the mount and the render, a Liveview usually also have a handle_event(event,socket) to handle clicks from the clients and/or a handle_info(event,socket) functions to handle messages from other actors. When the user clicks on the "send" button, handle_event() is called which dispatches the SendMessage command to the chatAggregate. When the read model state is updated, the state is broadcasted which triggers handle_info(). Here, the socket struct is updated, the render function is called, and the new message is shown to all users on the page.

#From the form submit
def handle_event("send_message", %{"message" => msg}, socket) do
:ok = LiveTea.App.dispatch(%SendMessage{chat_id: socket.assigns[:chat_id], sender: socket.assigns[:name] , content: msg})
{:noreply, socket}
end
#From the pubsub subscribed to in handle params
def handle_info(messages, socket) do
new_sock =assign(socket, messages: messages)
{:noreply, push_event(new_sock, "new_message", %{})}
end
def chat_page(assigns) do
~L"""
...
<form phx-submit="send_message">
<textarea id="textarea" placeholder="message; Shift-Enter for a newline"></textarea>
<button type="submit" phx-disable-with="Sending...">
Send
</button>
</form>
"""
end

Hope you found this interesting. I, for one, see this as an ecosystem that can do many things you now split into many different technologies (React, reddis, k8s, “mIcRoServices with LambDas”). There is a reason Whatsapp could serve 320 mill users with only 32 engineers, and my bet is that the reason is Elixir/Erlang.

Bonus: Go to https://livechat.stadler.no/dashboard to see live metrics.

[¹]: Server-side-initiated rendering is technically more correct, but rendered is what it feels like

Originally published at https://stadler.no/live_chat

--

--

Aksel Stadler Kjetså

A norwegian SW-dev with a MSc in mathematics/robotics. I’m working at Dignio , a remote care startup. My greatest achievement is taking 23 consecutive pull ups.