I stumbled onto Event Sourcing (ES) by accident in a recent, ongoing project. I discovered I had made (with some LLM assistance) a rough version of this architecture, having been vaguely aware of the concept a decade ago, but not realizing it until significant architectural decisions had been made.
The term CQRS (Command Query Responsibility Segregation) carries a lot of baggage with it, which does not resonate with me, but the foundation is super solid:
Commands mutate the system, typically generating a new read-only ViewQueries target the read-only ViewsIf you cleanly separate these two, which is often more difficult than it sounds, like maybe Pure and Impure functions, then some really nice benefits fall out for free. These are not formal architectures, exactly, more like families of architectures, community-defined architectures. There are lots of guidelines but not all of them are equally valuable.
Event Sourcing gives some similarly tight restrictions: You separate Events from State. The overall state of the system is modeled as a series of events, and events themselves must be simple facts without embedded or derived state. We use pure functional application to reduce / fold / materialize (all perfect synonyms, here) Events in order to accumulate intermediary or final States, or Views in CQRS nomenclature.
In a typical ES system, there is a "master stream" or "event log" or "system bus", append-only and immutable once written. It is not typically expected to materialize or query the master stream (from here on out: Event Log) though there is no reason not to (that I'm aware of), depending on scale. What is expected? Projections to derived streams -- filters, transformations, etc. As many projections as you like, for various purposes. Typically, the projected events are not written to a log, just reduced to a current State.
I think of a system bus, with projections branching off for specific concerns or subsystems. Almost like PubSub, where a Projection is like a Subscription but also implies using the opportunity to transform events as dictated by the needs of the subsystem.
I think in some CQRS views, you are supposed to always and only consume events. But in ES-world, you are expected to define reducers or fold operations on the projected streams (or the Event Log itself), in order to generate a queryable State/ View / Context (all perfect synonyms, here). For anyone who says you should not materialize the Event Log, I just imagine "the identity projection" and materialize that :wink:
What I am leading up to is an ES architecture with CQRS benefits. ES primary, and pick-and-choose your CQRS poisons. Command comes in to the Command Handler, which is responsible for generating Events to be serialized and written to the Event Log. The command handler, before or after writing to the Event Log, can feed "native" (avoiding serialize/deserialize round-trip) Events to Projection modules, which may or may not write their own logs. The Projection modules have Reducer (fold, etc) functions to accumulate Events into State (any number or complexity of such). This provides the Query interface for CQRS, and now we have come full circle.
Lot's of cool cross-platform package ideas to experiment with :smiley:
In my case, I write the Event Log to a file, in the Command Handler; I chose JSON for event serialization, for now, writing to .jsonl, but this is trivial to swap out for something more efficient. This has already proven useful for debugging. As noted above, the Command Handler feeds native Events to the defined Projections which own their Reducer functions. I wanted to avoid the terms State, Context, and View, so I chose to call my reduce operation Realization to an Aspect.
@doc """
Realize multiple events into the table aspect.
"""
def realize_events(aspect, events) when is_list(events) do
Enum.reduce(events, aspect, fn event, acc -> realize(acc, event) end)
end
The realize function pattern matches on the incoming event and performs an update to the current aspect. In Elixir, this pattern-matching operates via multiple function defs with different param shapes.
In my project, the Event Log is owned by a poker table, representing the house / dealer, performing all of the actions to maintain a valid game state for players (or bots). For house operations, I maintain a long-lived TableAspect and a short-lived HandAspect(per hand). Then we have a Player Projection and a Spectator Projection with derived event streams that hide information, and these each have TableAspect and HandAspect that are always up to date for queries.
So for a player-perspective poker room, you just need the Player Projection, and the TableAspect shows what the player can know about the state of stacks, the players at the table, the blind schedule, the rake, etc. The HandAspect tracks the deck, hole cards, board cards, wagers, etc.
The Table is a GenServer, which houses the Command Handler. Send a Command like CreateTable, and the Event Log has an event appended: TABLE_CREATED with some facts attached. No derived state lives in Events. When I said above that I had discovered this architecture, my first system incorporated a lot of derived state into events. This is very tempting! But I ended with a tangled mess that I subsequently archived, in order to build the second system with no architectural violations, and this is much cleaner!
As I was building out the event schema, I kept imagining myself as the consumer of events, and it was obvious that a little bit of "denormalization" would make event processing "much easier" or "more efficient". But you should instead design the system around the reducer, which is quite efficient at picking off (or ignoring) tiny facts and updating structs or maps or whatever you want to query. You should never query the current state and put that into a subsequent event -- this is literal circular logic that will bite you, ideally sooner than later. Treat it like cancer.
Architecturally speaking, the Command Handler may not know, a priori, which Projections to send to. I am working this out in my project, right now. But a runtime registration, via Observer pattern, is what I'm running with at the moment.
@Rick Hull I'm glad you bring up this architecture in the context of a card game (poker), because I have been doing something similar for a different card game (Lyn Rummy). Lyn Rummy is a rummy variation that we play in my family where the unique aspect of the game is that everybody puts runs and sets out to a common Board (using a capital "B" there for later) that everybody plays off. It's the funnest version of rummy that I have ever played, because when the Board starts having more than, say, 20 cards, you can make some pretty complex mutations to the Board to get your own cards down. For example, you may have a 10 that you're trying to get on to the Board, yet the Board may have no runs of 7/8/9 or J/Q/K that you can extend. But maybe there are sets of 9s and sets of 8s and then a 5/6/7 run where you can dump one of the stray 8s. You would then slice and dice the sets to get your card down.
I don't want to go on and on about the game itself, but hopefully this example gives you enough context to know that the fundamental aspect of the game is mutating the Board. That's where the fun is.
As much as I enjoy mutation for the game itself, I obviously prefer immutable data structures on the programming side. This particular game is written in TypeScript, but I have also written small games in Elm before, and obviously Elm's architecture is at least somewhat close to CQRS in spirit, if I understand the concept correctly. So even though it would be pretty easy for me to mutate almost every data structure in TypeScript and get the game working correctly (just because the rules of the game are so finite), I choose instead to make most of my objects immutable. So, for example, if I move a stack of cards on the Board (by dragging it to a new place), I don't simply updates its loc.top and loc.left; instead, I create a brand new copy of a CardStack that has the same suit and value, and then I generate a game event to remove the original CardStack and add the new CardStack. But I don't want to get too hung up here on mutable vs. immutable, because I don't think that's the main gist of your idea. This is all still kinda context.
The event management piece comes in where I treat literally every move during the turn as a GameEvent and I route everything through the EventManager. Whether I'm dragging a hand card to the board or dragging it directly to an existing set or run, or whether I am joining two piles on the board or breaking up a pile or moving a pile, I always build this data structure:
type BoardEvent = {
stacks_to_remove: CardStack[];
stacks_to_add: CardStack[];
};
type GameEvent = {
board_event: BoardEvent;
hand_cards_to_release: HandCard[];
}
And CardStack is an array of BoardCard objects plus a BoardLocation object with top and left.
Then the only other fundamental events of the game are when the players complete their turn, and that is essentially just a single command with no real metadata. There's also a rollback/undo mechanism, but I think of that as living "outside" the architecture, although, of course, the ease of implementing "undo" fundamentally depends on how simple your architecture is!
Anyway, I haven't gotten yet to the multi-player piece of my game, but I think that's where my architectural decisions to route everything through an EventManager and to model almost every event of the game as explicitly being a GameEvent is going to pay off big time. For example, if somebody wants to join a game in the middle, we don't need to send them a snapshot at t=now; instead we send them the original board plus all the events that have been recorded up to t=now and then let them reduce it on their side. As long as the performance is fine, this should be fairly simple.
Anyway, I'm not sure if I am providing any extra insight here, but I wanted to let you know that you gave me some food for thought, and there are probably other folks authoring card games who are wrestling with the same kinds of questions.
Rick Hull said:
If you cleanly separate these two [Commands and Queries], which is often more difficult than it sounds, like maybe Pure and Impure functions, then some really nice benefits fall out for free.
That statement really resonated with me! Here is an excerpt from a conversation with my co-author of the rummy game:
I'm using deltas from the very beginning of my architecture. It's just the case that in my early version of the implementation (i.e. nothing is going over-the-network yet nor am I doing any kind of fancy undo or replay-the-whole-game logic) I only use the delta once.
The beauty (and danger) of the delta approach comes when you apply O(N) deltas for various features. The beauty is that you almost get everything for free. The main dangers are a) performance and b) edge cases. The former is self-evident; the latter may be more subtle. If you get into any kind of strange edge case where applying deltas makes things really ugly (can't think of a good example yet, but that's what makes the danger even more insidious!), then the whole thing can tumble like a house of cards.
(no pun intended)
Ha, I am going through something kind of similar at the moment; while I can replay the Event Log, I am trying to avoid doing that in normal operation, if I can. So I am using the "current snapshot" (Aspect) for any players that join an existing session (which is really, all of them, in the current design). My LLMs had, unbeknownst to me, been implementing with Event Log replay, and in a particularly inefficient way, and I'm working through the Observer pattern of runtime-registration, event broadcast to all registered observers, and then the synchronization issues that follow (draining mailboxes, whose turn is it, etc.). This got uglier than I wanted, and buggy, and I've tamed it by introducing a Dealer GenServer, who exclusively issues Commands to the Table, and is then responsible for Event broadcast and synchronization. It seems to be working pretty well now.
Using the Aspect to give newly-joined players a starting state seems to be the right call for my current design; so long as the moves stay synchronized, I have an incredibly robust game design.
But in building this from the bottom-up, I had baked in some assumptions about synchronization which are now violated by the Observer broadcast, so what's left is to have all the moving parts make sense and stay synchronized, and my Dealer seems to be doing this.
Yeah, I foresee a Dealer in my future.
For the replay-game feature, there are certain UI elements that I will want and need to preserve (because I actually want to animate the moves!) and certain UI elements I will want to suppress (sound effects and some popups) and certain UI elements that will be impossible to reproduce (the actual dragging of the card to the hand). So even though, in principle, replaying the events will be completely elegant and trivial (particularly in the model code), the actual mechanics may get ugly quickly.
My plan calls for Phoenix and a web poker ui, but I am just working on the core, with CLI scripts, and small chunks of text, so the Event Log tells the entire story, and my anticipated web UI in any given "frame" is based purely off of an Aspect, but I could do Event-based UI animation on top of that.
The model for my game is quite simple, because I don't have any wagering. So I have already gotten myself immersed in the UI code. I have UI objects that mutate themselves in place during drag and drop events (essentially just changing their background and doing similar styling effects to show that they are legit targets for dropping a card or card pile to extend themselves, e.g. allow the 6 to drop on to their 7/8/9 run), but other than that, my UI is really just redraw-the-world after any significant event.
Having the UI just redraw itself completely after every event definitely simplifies the code, and you can get away with it at the scale of card games. My game is played with 2 decks and at most 5 or 6 players, so the total number of DOM objects is like a couple hundred at most.
Phoenix was just a city in Arizona until I found this link: https://www.phoenixframework.org/
(posting for convenience to others...I suspect I am not the only person here who hasn't played with Elixir yet)
Those bastards at Discord built on Elixir, AIU :wink:
The tool we're using now is built on fricking jQuery and Handlebar templates. It's so gross.
I'm building my card game directly on the DOM API.
I started this poker thing in new-Roc but got bogged down in learning, lack of libs, and some bugs. I had a good bit of fun here though, implementing Events as S-expressions (Lisp) rather than JSON.
Then, I almost went with TypeScript, either bun or deno, as I anticipated using JSON as some kind of lingua franca, but I am a JS-hater and Elixir-lover, so after agonizing with some LLM perspectives and counterpoints, I decided to pick Elixir back up after nearly a decade of inactivity there. It came down to: Elixir's typing is strong enough (for my personal explore-with-types-as-a-ruby-guy goals) for the scale of game that I'm building, and dat Phoenix web framework -- channels, LiveView, etc.
Yeah, that's fair. I actually hate JS way less than the average person, and I will build any toy app that's less than 1000 lines long (you can do a lot with not much code) in pure JS. But TypeScript kinda rules for apps in the 3k-to-5k range. You have a tiny extra build step, but that's relatively painless. TypeScript understands the DOM API, so it catches silly mistakes like using div.backgroundColor = "blue" when you should have written div.style.backgroundColor = "blue".
Learning Elixir is definitely in my queue. Erlang too.
Phoenix, on the other hand, isn't really on my radar. I basically despise anything that reeks of HTML templates. I'm old-school that way. HTML is a documentation language. It's assembly language for true components. Every component in my app is either a pure function returning a DOM snippet or a proper class when I need mutation. And everything is just pure composition. No implementation inheritance required either.
And everything is just pure composition. No implementation inheritance required either.
Preach! I learned this the hard way in Ruby. I can and will reach for inheritance, but only in a tiny minority of situations.
Every component in my app is either a pure function returning a DOM snippet or a proper
classwhen I need mutation
This idea is fairly new to me, building-directly-on-the-DOM.
At risk of repeating myself, I drew a lot of inspiration from building web apps in Elm. I don't really like Elm at scale, especially for things like drag and drop, but that's just a personal preference thing. Elm can actually do things quite nicely that you would think would be difficult under its paradigm. I just have a lot more recent TS muscle memory, and I like being a bit closer to the native stack.
Rick Hull said:
Every component in my app is either a pure function returning a DOM snippet or a proper
classwhen I need mutationThis idea is fairly new to me, building-directly-on-the-DOM.
It's new to a lot of people! Blame Microsoft Internet Explorer for all the unnecessary technologies that were built on top of the perfectly workable DOM API. And blame some of the earlier browsers too.
I re-read your initial stuff about Lyn Rummy (what does Lyn refer to?), and I can see how the board has a physical representation, vs. poker's logical representation, and that has some implications for UI state.
From what I wrote about ES and CQRS, would you say Elm is more CQRS oriented? CQRS is "always" event-based, but maybe not with the same premises as ES.
Lyn is actually my Aunt Lyn! I don't think she invented the game, but she popularized it within our family, so we have been calling it Lyn Rummy since at least the late 1980s.
I actually do have both a logical representation of the board (called Board) and physical representation of the board (called PhysicalBoard). The logical representation of the board necessarily tracks the location of the cards on the board down to the pixel, so even the "logical" board has physical knowledge (unlike most versions of poker that I know of, where the location of a card on a table is more logical in nature, e.g. it's the "river card" or something). But the PhysicalBoard class adds more UI features like click handlers and drag/drop support, as well as rendering all the DOM objects.
Rick Hull said:
From what I wrote about ES and CQRS, would you say Elm is more CQRS oriented? CQRS is "always" event-based, but maybe not with the same premises as ES.
Yes. My Elm is rusty, but Elm literally has an Html.Events model. It's a little more low-level than what we're talking about, but you can kinda build on top of it with your own Msg objects. I can dive deeper on this if you'd like.
You can actually play the game now! The solitaire version is basically code complete: https://showell.github.io/LynRummy/
Now I'm getting to the real fun parts! Network play and fancy gizmos to replay the entire game on an animation loop. :slight_smile:
I'll check it out, when I'm unburied :wink: :thumbs_up:
Rick Hull said:
And everything is just pure composition. No implementation inheritance required either.
Preach! I learned this the hard way in Ruby. I can and will reach for inheritance, but only in a tiny minority of situations.
Missed this earlier! Yeah, I'm not 100% religious about this, but once you learn how easy it is to just write one-line wrapper functions that forward events to the sub-components within an object, it's every bit as convenient to the callers, pretty rote for the class itself, and you just avoid tons and tons of coupling and action at a distance.
you just avoid tons and tons of coupling and action at a distance.
Rick Hull said:
I started this poker thing in new-Roc but got bogged down in learning, lack of libs, and some bugs.
Yeah, I wish I had better timing in joining this community. Writing the server piece for my card game was going to be my first foray into Roc, but I'm kinda waiting for the dust to settle a bit on the new compiler.
I'm an ex-Zulip developer, so for now the server piece is gonna be Zulip. The game coordination will mostly be peer-to-peer in nature, and I am just gonna have the clients validate incoming events as if they don't trust the server. In the TS ecosystem you can use zod for that, or even home-grow your own schema checker. Like I said, the game itself is pretty simple, and I am trying to be good about having very simple type shapes for my game events.
The Zulip server will just be a broadcast mechanism (its main job is to distribute messages to clients) and an authentication server (but that's the sucky part for getting new users).
OMG that brings back memories! The GvR program was inspired by Karel the Robot. My CS 101 teacher taught us Karel the Robot for the first week of class, and he would simulate the robot on stage while he taught. (It was a big lecture class, so he had suitable room to "turn left". Kind of like Derek Zoolander, Karel had to work to achieve this.)
Ahem, https://en.wikipedia.org/wiki/Karel_(programming_language)
I've started to work on the replay-events feature. Believe me, you get punished for every hack. I recommend trying to get a replay mechanism going early in your app, as it will reveal janky constructs. The good news is that I kinda have something working finally, and it's pretty sweet.
With the ES discipline, generally you get replay "for free". As for me, Table is a GenServer that owns the event log and realizes the event log to the two aspects, table_aspect and hand_aspect. Dealer is another GenServer that is primarily the Command Proxy or Gateway, insulating the Table from the players and spectators. Only the dealer can mutate the table, but players command the dealer.
Thanks to CQRS, players can "read the table" (aka query a table aspect) without going through the Dealer. As noted, Players and Spectators do runtime registration to the dealer, and then when the dealer sends a command to the Table, the Table writes the event_log with JSON events, but returns native events to the dealer. The dealer broadcasts these events to all registered projections.
The projections live in a 3rd execution context (Table GenServer, Dealer GenServer, everything else), and can possibly have an execution context per projection / player / spectator. Thanks to OTP (Erlang's stdlib, kinda), the Dealer can send messages to the "script context" (CLI) or "player context" (Web), which have simple and idiomatic ways to respond.
This gives synchronization; the Dealer has a command loop which keeps commanding the table to the next state, until the table is waiting for player action. The Dealer and Table know which player is to act, and using the actor/mailbox system, the Dealer solicits an action from the correct player, and the command loop continues.
I've never (at least explicitly) worked in an actor/mailbox system. Sounds fun!
Ok, here's the event playback in action:
https://gameprogrammingpatterns.com/command.html is relevant here. I actually found that online book by going down a rabbit hole from #off topic > found this book, haven't read it tho
And of course the author references the Gang of Four (Design Patterns), which I assume Martin Fowler was at least partly influenced by as well.
Steve Howell said:
Lyn Rummy is a rummy variation that we play in my family where the unique aspect of the game is that everybody puts runs and sets out to a common Board (using a capital "B" there for later) that everybody plays off. It's the funnest version of rummy that I have ever played, because when the Board starts having more than, say, 20 cards, you can make some pretty complex mutations to the Board to get your own cards down. For example, you may have a 10 that you're trying to get on to the Board, yet the Board may have no runs of 7/8/9 or J/Q/K that you can extend. But maybe there are sets of 9s and sets of 8s and then a 5/6/7 run where you can dump one of the stray 8s. You would then slice and dice the sets to get your card down.
At risk of adding nothing technical to the discussion, this sounds exactly like Rummikub which I used to play with my family a lot. You use tiles which makes the reconfiguring easier, and they just number them 1-13. But it's basically what you described.
Yeah, I've never played Rummikub, but it sounds incredibly similar. Here's another link to my app, for convenience: https://showell.github.io/LynRummy/
Obviously, under the hood, the cards are numbered 1 to 13. We have some twists that might not be in Rummikub. You can go "around the ace", so J-Q-K-A-2 is allowed (aka 11-12-13-1-2). And we allow red-black alternating runs, where red is H/D and black is S/C, of course.
Last updated: Jun 16 2026 at 16:19 UTC