Temporal and CQRS

Hi,

Very interesting new technology, thanks for all your work and for sharing.

Can someone please explain if/how Temporal could be used to implement CQRS (with Event Sourcing) or, with a gulp, does this replace CQRS?

I can see Temporal being great for the Write Side (Domain Model) but I can’t understand how the Read Side (View Models) may work.

In essence, keen to hear thoughts on CQRS/ES and Temporal.

Thanks,
Ashley.

2 Likes

I’m not an expert on CQRS, so correct me if I misrepresent it.

Temporal is not a technology to implement CQRS. It is a technology that can be used instead of it for a large number of use cases.

My understanding that the main reason for using CQRS is a desire to build an event based architecture. But it comes with many drawbacks. Mostly around eventual consistency and being a pretty low-level abstraction for developers to deal with directly. CQRS by itself is just a pattern. It still requires choosing particular implementations of event storage, durable timers, and associated client-side libraries.

Temporal is fully event-based and asynchronous. It is an architectural style, programming model, and concrete production-ready implementation. Temporal abstracts out most of the complexity of building and operating resilient processes from short to never-ending ones. Internally Temporal uses event sourcing for recovering program state, but it is more like implementation detail that does not affect directly how applications are designed and written.

I can see Temporal being great for the Write Side (Domain Model) but I can’t understand how the Read Side (View Models) may work.

In Temporal a unit of computation is called Workflow. The workflow can contain a very complex domain model. Workflow has its own threads to drive activity executions and it can react to asynchronous external events. It supports read operations to its state directly through a sychronous query feature. Temporal workflow is fully consistent. So it doesn’t have a problem that CQRS has with eventual consistency of view models.

Temporal allows creating service oriented architectures that consist of workflows and activities that are hosted by different services and invoke each other. You can think of Temporal as a service mesh that supports operations of unlimited duration.

1 Like

Thank you for your reply Maxim.

There are many variations of CQRS but I would say most enterprise implementations use a distributed architecture and thus face all of the issues that Temporal addresses. The most common variations also use event-sourcing to build and rebuild domain aggregates (entities) when needed to apply commands on them, which Temporal also does (amongst other things).

However, these variations also use the event-stream to build and rebuild the current and potentially new (and often denormalised) read-sides (view models) as / when needed. I guess a Temporal workflow for an aggregate could use an activity to add an event to an external event stream that was used by one or more other workflows to build or rebuild one or more read-side view models.

A Temporal version of CQRS wouldn’t need to worry about rebuilding the aggregates. However, it seems somewhat redundant to recreate an external event stream that’s similar to one already created by Temporal and used to rebuild the state (and other part of the apps). Of course, I am not sure what form the event-sourcing in Temporal takes and it builds much more than just the domain state.

One of the advantages of the popular approach to CQRS with Event-Sourcing is also that the write-side and read-side are able to scale independently. As we know, many applications have a much higher load on the read-side (queries) that the write-side (commands to change the aggregates). Can Temporal scale it’s handling of queries (read-only) for different aggregates (entities)?

The query feature is pretty cheap in Temporal as it doesn’t involve persistence writes or even reads if a workflow is already cached on a worker. So I would say that Temporal indeed scales better for reads than writes. If you want to sacrify consistency for performance we can always add a cache for query results to optimize it even more.

I can see Temporal being great for the Write Side (Domain Model) but I can’t understand how the Read Side (View Models) may work.

I guess a Temporal workflow for an aggregate could use an activity to add an event to an external event stream that was used by one or more other workflows to build or rebuild one or more read-side view models.

I would use Temporal for write side and another system for reads. IMO Apollo Federation is the best system for distributed reads. The read system may source some data from Temporal through Temporal Queries—e.g. when that piece data lives in the state of running Workflows—and the rest from OLTP DBs or read DBs. Temporal may be writing data to those DBs—for example, at the end of a Cart Workflow, after a successful checkout, the last activity may create a record in the Orders DB. If you want a denormalized read DB, for example OrdersWithUserAndItems, to then be updated, there are a few ways:

  1. The Cart Workflow could have another activity at the end that does the update.
  2. You could use a CDC-based system like Debezium to get OrdersWithUserAndItems updated after the Temporal activity inserts into the Orders DB.
  3. The method you mentioned: an activity at the end of the Workflow emits an OrderCreated event to Kafka, and a consumer does the DB update.

For rebuilding OrdersWithUserAndItems from scratch, replaying the event logs of #2 and #3 are straightforward. For #1, the event log is there (an ActivityTaskScheduled event in the Cart Workflow history with timestamp and input arguments), but replaying it is not straightforward—you can’t just tell Temporal “rerun all update-OrdersWithUserAndItems activities in the order they were executed”. You can only run new Workflows. So anything you want to be re-runnable should be a Workflow. So if you wanted to do the update logic inside Temporal, instead of #1, I’d do a modified version of #3:

3b. At the end of the Workflow, start an OrderCreated Workflow (starting a Workflow is effectively emitting an event) that does everything the OrderCreated consumers did in #3, including updating OrdersWithUserAndItems.

Then rebuilding OrdersWithUserAndItems would be replaying the OrderCreated Workflows in the same order with the same arguments. I haven’t tried this, and I’d be inclined toward your #3 over 3b.

More generally re: how having an event-sourcing-based system fits with Temporal, while Temporal internally uses event sourcing, there are some differences and limitations of using Temporal’s internal log compared to using Kafka history. And usually not all writes go through Temporal—in that case, in order to have event sourcing that covers all events/actions, it needs to be done outside Temporal; for instance, all events go to Kafka, and some consumers start Workflows.

2 Likes