DEV Community 👩‍💻👨‍💻

Dominique Schönenberger
Dominique Schönenberger

Posted on • Updated on

Real-time frontends with Kalix and Laminar

Events are very good to keep systems in sync. Here I keep the backend and the different frontends (web, mobile) in sync thanks to events and Scala everywhere.
Having everything in sync at any time is what I call here real-time frontends.
Events are broadcasted and pushed to frontends through web sockets. Events are replayed with event sourcing in backend and frontend.

Image description
In order to correctly keep everything in sync, it is important to replay the events in the same way on every sides. For that purpose, in this proposal, we suggest to the same definition of events and the same scala code for the replay share between backend and frontends.
This sharing not only help for having things in sync but also to avoid redefining the same thing again at frontend. Very often, we define data model in the backend and then the same in the different frontends. Here we would like to avoid that and define it once in Kalix and reuse it in the Laminar frontends (web, mobile).
Reusing the exact same scala code for the replay is really important. It saves you from out of sync problems. If you have different codes (probably also different languages) for the replay between backend and frontend, you start quickly to have big problems, bugs and data problems. Think of a mobile app you need to redeploy to fix those problems (Mobile deployment is not straightforward).
To illustrate my point, I have created a small example. The domain is crowd funding. The replay logic is very simple and minimal but it's just an example.
Code can be found here: https://github.com/domschoen/real-time-fes-kalix-laminar-example
The repository contains 3 projects:

  • Kalix project
  • Laminar project for the web
  • Laminar + Capacitor project for mobile app (not yet available)

Note: For more information on how to run the example, look at the readme of the different projects.

Description of the example

The app manage crowd funding projects. A project has:

  • Project ID
  • Title
  • Description
  • Goal: The amount of money you want to achieve with your crowd funding project
  • Funded: The amount of money collected so far

You manipulate the projects with 2 actions:

  • Change project details (Title, Description, Goal)
  • Invest: give money to the project

What is shared ?

We share:

  • Events definition as protobuf
  • REST entry point arguments as protobuf
  • State (result of replay of events) as protobuf
  • Replay events logic as Scala file

We simply have unix links from Kalix project to Laminar projects.

Cherry-picking the shared parts

In the frontend, we are in a different context and we cannot just share the whole directory of Kalix protobufs or the whole file containing the replay. We have to isolate the parts we are interested in and restructure a little things in Kalix by moving what we need in separate files.
This denature a little the Kalix project but may be in future version of the project, we could come up with a better solution where we share things as they are and find a way in the frontend to ignore what we don't need

Keep everything in sync

I have identified 2 solutions:

  1. When Kalix emit an event, it is send to Kafka. Each frontend register to Kafka and replay event received.
  2. A frontend emit a command to Kalix. If succesful, it sent the related event (as it also created in Kalix) to the server of the frontend via websocket. The frontend server broadcast it to all frontends via websocket.

The first solution is more elegant and work also with commands issued directly to Kalix (not from Frontends only) but I choose the second one just to avoid Kafka for the moment and come up rapidly with an example.

Laminar

The core of the frontend is a Laminar EventBus:

  • EventBus receives events and snapshots (more about snapshot below) either from Kalix or from the broadcast of event from other frontends.
  • From the EventBus we go through the replay which gives as output a Signal containing the list of all projects. Laminar is just perfect to do that ! as you can see in this code:
  val $projects: Signal[List[ProjectData]] = 
    eventBus.events.foldLeft(
      initial = List.empty[ProjectData])
      ((acc, ev) => {
        ev match {
          case evt: ProjectState =>
            replayEvent(evt.projectId, evt, true, acc, snapshotReplay)
          case evt: ProjectDetailsChanged =>
            replayEvent(evt.projectId, evt, false, acc, ProjectReplay.projectDetailsChanged)
          case evt: MoneyInvested =>
            replayEvent(evt.projectId, evt, false, acc, ProjectReplay.moneyInvested)
          case _ =>
            println("Bus no managing event " + ev)
            acc
        }
      }
    )
Enter fullscreen mode Exit fullscreen mode

List of all projects in a Laminar Signal

The structure of the projects in the list which is maintained in the Signal is twofold:

  • The state
  • The list of events

The state is always present but the list of events is optional.
When we fetch data from kalix, we have 2 use cases which has been chosen for performance reason:

  1. When we display the list of all projects (AppProjectsPage), we fetch a list of states (= snapshots in the replay of events). Why ? because it would be too costly to fetch all events of all projects and replay all those events. In that case, each project structure contains only the state, not the events part.
  2. When we look at one project (in the Project Dashboard page), we fetch all events of the project and replay them. Here the project structure contain the state (result of the replay) and the events.

You may ask: Why would it be interesting to have the list of events in a first place? The response is: We can display the project events to the user and it represent the the history of the project (Audit trail).

This was for data fetched from the backend but what happen when an event is received from another frontend ? We update the state using the replay logic and we add the event to the list of events in the project structure.

Conclusion

Laminar is really helping to stream events across frontends and inside frontends and really allow to have easily all frontends in sync.
This solution should save you development time and make the synchronisation between backend and frontends more reliable.
There are many areas of improvements:

  • How to manage lots of data which does not fit the frontends ?
  • In a bigger application, with multiple entity, you need to segregate your events.
  • How to manage unreceived events ? In a web app, user can always refresh the page to get fresh data but that is not really a solution and mobile app may keep state in a longer run.

Don't hesitate to give me your feedbacks to: dschoenenberger@mac.com

Top comments (0)

Stop sifting through your feed.

Find the content you want to see.

Change your feed algorithm by adjusting your experience level and give weights to the tags you follow.