August 14, 2023
Why Cirqus, and Why Now?
I have been working on Evently for a few years now, building out the foundations of the event ledger. Evently has a nice CLI to help developers inspect their ledger, a stable REST API, and a front-end-friendly notification system for real-time event subscriptions. In all, the bones of Evently look pretty good. It’s time to move forward from the “what do you think?” preview mode we have been in and on to a usable service to build applications.
That means we have to add in account and service management capabilities. As I started to design these, it became obvious that this work would be much easier to write as an Event-Sourced application that used CQRS concepts like commands, read models and the event ledger to store everything meaningful. In other words, Evently should use event-sourcing to build up the Evently offering. We need to eat our own dog food, and the dog food of choice is an application framework.
Over the years, I have built CQRS/ES application frameworks in different programming languages and have used several others, so the thought of building a new one is both familiar and exciting. I have made numerous mistakes and learned many lessons from others along the way. Evently has some unique capabilities that don’t quite fit inside existing frameworks, such as Filter Selectors, Atomic Append and Notifications, so what I know today is about 50% of what needs to be known in order to create a useful and compelling framework for Evently.
Naming is hard, so that seems like a good place to start. After a lot of fussing about with whiteboards, sticky notes and talking out loud, I’m settling on calling it Cirqus. The letters CQRS/ES do not turn into a simple term (though I did give Querqus a good go), and finally settled on the common software practice of misspelling things. Evently isn’t a word, either, so it fits the pattern.
Cirqus Key Design Principles
Permalink to “Cirqus Key Design Principles”Front-End Friendly
Permalink to “Front-End Friendly”Evently focuses on reaching beyond the server-side, out to the front end. In this day and age, that means Javascript, or at least a JS-friendly interface. The framework will run in the browser as well as in server-side node deployments. It should bear a minimal footprint and use modern concepts as well as pair well with tree-shakers and compilers like Webpack.
Declarative
Permalink to “Declarative”Nobody likes writing boilerplate; it’s easy to mess up, and it adds no value. We have some really nice options now with decorators and ESM to declare relationships and reactions in code, rather than wiring things together manually. For example, here are some examples of what your application code might look like as declarative reactions.
Read Model
Permalink to “Read Model” {copyText = 'Copy'}, 300)" class="language-typescript">// ESM lets us import JSON files safely
import GuestRegisteredSchema from "./events/guest-registered-schema.json" assert {type: "json"}
// Types generated by json-schema-to-typescript
import {GuestRegistered} from "../../types/guest-registered-schema"
import GuestDeletedSchema from "./events/guest-deleted-schema.json" assert {type: "json"}
import {GuestDeleted} from "../../types/guest-deleted-schema"
// the Read Model type
export type GuestModel = {
firstName: string
lastName: string
email: string
state: "registered" | "deleted"
}
export type GuestModelVariables = {
email: string
}
// marks the class as a read model
@ReadModel({
name: "registered guests",
// Evently selector used to fetch events
selector: {
data: {
guest: {
// Uses the schema's Title as the event name.
[GuestRegisteredSchema.title]: "$.email ? (@ == $email)"
// Notice the '$email' var? This read model reuses the selector statement
// by using data from other call sites to fill in the request.
}
}
}
})
export class GuestEntityReadModel implements ReadModel<GuestModel, GuestModelVariables> {
// react to an event in the stream.
@OnEvent(GuestRegisteredSchema)
// model is the hydration result
onGuestRegistered(model: GuestModel, event: GuestRegistered): GuestModel {
const {firstName, lastName, email} = event
return {
firstName,
lastName,
email,
state: "registered"
}
}
@OnEvent(GuestDeletedSchema)
onGuestDeleted(model: GuestModel, event: GuestDeleted): GuestModel {
return {
// spread operator makes hydration easy.
...model,
state: "deleted"
}
}
}