Open-source project
gvolpe/trading avatar
gvolpe/trading

The trading reference app splits a Scala 3 event-driven system into nine modules

πŸ’± Trading application written in Scala 3 that showcases an Event-Driven Architecture (EDA) and Functional Programming (FP)

663 stars77 forksScalaApache-2.0

At a glance

What is it?
gvolpe/trading is a reference application for a book on functional event-driven architecture, written in Scala 3 with Elm and Tyrian front ends, backed by Apache Pulsar, Redis, Prometheus and Grafana. Its value is as a worked example of a service-per-module layout with shared domain types, and its cost is that running it needs a message broker, a cache and three build paths.
Who is it for?
Adopt this repository as a reference if you are learning how a functional event-driven system is split into small services with a shared domain module, or if you want a Scala 3 and cats-effect example that runs end to end. Do not adopt it as a production starting point, because it is written as a book reference, the only tagged release is v1.0.0 from 2023, and it needs Pulsar, Redis and a full container stack to start.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 6 days ago.
What is it written in?
Mainly Scala, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Nine modules, of which five are deployable services

The back end is a mono-repo, and the README splits it into nine modules, five of which are deployable applications and three of which are shared modules, plus a demo module and a web application. The tree in the README lists them:

code
modules
β”œβ”€β”€ alerts
β”œβ”€β”€ core
β”œβ”€β”€ domain
β”œβ”€β”€ feed
β”œβ”€β”€ forecasts
β”œβ”€β”€ it
β”œβ”€β”€ lib
β”œβ”€β”€ processor
β”œβ”€β”€ snapshots
β”œβ”€β”€ tracing
β”œβ”€β”€ ws-client
β”œβ”€β”€ ws-server
β”œβ”€β”€ x-demo
└── x-qa

The service names tell you the shape of the system. Feed publishes market data, processor consumes it, forecasts and snapshots derive state from it, alerts turns state into notifications, ws-server pushes to browsers, and tracing exposes observability data. The x-demo and x-qa modules are prefixed to mark them as examples rather than production services, and the it module is the integration test suite.

The shared modules, domain and lib, are the interesting part for anyone learning the architecture. Domain holds the vocabulary the services exchange, and lib holds the shared infrastructure. In an event-driven system the type definitions in domain are effectively the contract between services, so having them in one place rather than duplicated per service is a deliberate choice.

There is also a web-app/ directory at the repository root, and modules/ws-client, which is the Scala side of the browser client. The README's table of contents lists the services as Lib, Domain, Core, Feed, Forecasts, Processor, Snapshots, Alerts, WS Server and Tracing, with separate sections for the tests, the two x modules, monitoring and topic compaction.

Pulsar carries the commands and events, Redis carries the state

The requirements are two running services, Apache Pulsar and Redis, and the README says a provided docker-compose.yml makes this easier. The overview diagram has a legend that tells you what the arrows mean: dotted lines are Pulsar messages such as commands and events, and bold lines are reads and writes to and from external components such as Redis and Postgres.

That split is the architectural point of the whole repository. Services do not call each other. They publish commands and events to Pulsar topics and consume from them, and anything that needs to be stored or looked up goes to an external store, with Redis in the picture for the compose setup and Postgres named in the legend as another example of an external component.

The compose file shows how concrete this is. The pulsar service runs the apachepulsar/pulsar:2.10.1 image with the command bin/pulsar standalone, publishes ports 6650 and 8080, mounts a standalone.conf from the pulsarconf/ directory, and has a healthcheck that curls the admin brokers health endpoint every two seconds with a two minute start period. The redis service runs redis:6.0.8 with DEBUG=false, a redis-cli ping healthcheck every second, and thirty retries.

Both sit on a bridge network named app, and the monitoring services attach to it as well. The pulsar-manager service is worth noting because of its dependency condition: it declares depends_on pulsar with condition service_healthy, so it only starts once the broker reports healthy rather than merely when the container is created.

Three ways to build the base image, and one of them is broken

Before the service images can be built, a base image named jdk17-curl has to exist on the local Docker server, because the compose file depends on declared services being published locally. All docker builds are handled inside build.sbt using sbt-native-packager, and the README offers three routes to that base image.

The first is a plain Docker build using the provided modules/Dockerfile:

shell
$ docker build -t jdk17-curl modules/

The second builds a slim image with Nix, also based on openjdk:17-slim-buster, and the third builds a layered image from the same JDK used for development. Both Nix variants end with docker load, and the difference between the three options is the resulting image size, which the README shows with a docker images listing where the three jdk17 variants sit in the 422MB to 557MB range.

One of those routes has a documented problem. A note states that as of September 2022 the Docker image resulting from nix build .#docker is no longer compatible with sbt-native-packager, so you should either go for nix build, which defaults to the slim image, or build directly via Docker with the given Dockerfile. That is a real trap for anyone following the README in order, since the third option is the one that no longer works.

With the base image in place, publishing all the service images is a single sbt command, docker:publishLocal, and the README is explicit that any of the three image variants is valid, so pick the method that fits your toolchain.

The feed service is the one you run from sbt

Starting the stack is a single compose command, and the output shows the shape of the deployment: the broker, the cache, the ws-server, the pulsar-manager, the alerts, processor, snapshots and forecasts services, plus the tracing service, Prometheus and Grafana, all on a network called trading_app.

That list omits one module on purpose. The README recommends running the feed service directly from sbt whenever necessary, because the feed publishes random data to the topics that other services consume messages from. So the data source is a development tool rather than a deployed service, and everything else is a consumer of the topics it writes to.

This is the kind of detail that makes a reference application worth reading. The producer of the market data is synthetic and run locally, while the consumers, which include the web socket server, the alerting service and the snapshot and forecast services, run in containers.

There is also a Kafka variant. The repository ships a separate kafka.yml, and to run the Kafka demo the README says that file should be used instead of the default compose file, with docker-compose -f kafka.yml up. The topic list also includes a pulsar-cdc.yml at the repository root, which is the change data capture configuration for the broker, and a topic compaction section in the table of contents, so the repository covers the operational side of streaming rather than only the happy path.

The Elm client and its Tyrian replica are built in different ways

The web application lets users subscribe and unsubscribe from symbol alerts such as EURUSD, emitted in real time over Web Sockets. The primary front end is written in Elm, and the README gives a Nix build:

shell
$ nix build .#elm-webapp
$ open result/index.html # or specify browser

There is also a development shell for local work, where you compile the Elm source directly with elm make and open the resulting file. If Nix is not what you want, the README says to install Elm by following the official instructions and compile as usual with elm make src/Main.elm --output=Main.js.

A second front end exists as a replica of the Elm application written in Scala with the Tyrian framework, built with sbt 'webapp/fullLinkJS'. The README explains the choice: fullLinkJS produces a fully optimized JavaScript file, while fastLinkJS gives faster iteration. It also notes that the nix run command will create a directory for the Parcel cache which needs write permissions.

For iteration without Nix, the README falls back to yarn and parcel in the modules/ws-client directory with yarn install, yarn build and yarn start. It marks that path with a caveat, saying it is not fully reproducible and cannot be guaranteed to work in the future. The Nix route is the reproducible one, and the README is candid that the yarn route is the fragile one.

Monitoring, tracing and the CI matrix are all in the repository

The observability stack is not an afterthought here. The compose file starts Prometheus from the prom/prometheus:v2.30.3 image with a configuration file mounted from the monitoring/prometheus/config.yml, and Grafana is in the same stack, and both attach to the app network alongside the services. A separate pulsar-manager service runs apachepulsar/pulsar-manager:v0.2.0 with its own properties file mounted, on ports 9527 and 7750.

The topics list includes open-tracing and grafana alongside prometheus, and the README has a dedicated Tracing section and the modules include a tracing service, so tracing is a first class service in the architecture rather than an agent bolted onto the others. A separate monitoring/ directory holds the configuration for these components.

The CI badges at the top of the README are just as telling, because there are five of them and they cover different parts of the build: an Elm workflow, a Scala workflow, a Tyrian workflow, a registry workflow for images, and a Smokey workflow on the main branch. That last one is the end to end check, and the fact that the front end is built and tested three separate ways, Elm, Scala and Tyrian, reflects the two front end implementations in the repository.

Maintenance is the one weak spot in the story. The last push was on 2026-10-18 and the repository is not archived, so the code is current, but the release list holds a single entry, v1.0.0 from 2023-09-03, and the README describes the project as a reference application developed for a book, so the commits are where the activity is, not the tags. The licence is Apache-2.0, recorded in the LICENSE file at the repository root, which is a permissive grant with no copyleft obligation on code you reuse, a licensing note rather than legal advice.

Editorial conclusion

Adopt this repository as a reference if you are learning how a functional event-driven system is split into small services with a shared domain module, or if you want a Scala 3 and cats-effect example that runs end to end. Do not adopt it as a production starting point, because it is written as a book reference, the only tagged release is v1.0.0 from 2023, and it needs Pulsar, Redis and a full container stack to start. Verify first that the module list and the compose file match the services you actually need, that the Pulsar and Redis images in docker-compose.yml are acceptable to you, and that you can build the base jdk17-curl image, since every service image depends on it.

Frequently asked questions

What does the gvolpe/trading repository contain?

It is a Scala 3 reference application for functional event-driven architecture, split into nine modules of which five are deployable services and three are shared, plus a demo module and a web application. The front end exists as an Elm app and as a Scala replica built with Tyrian.

How do I run the trading application locally?

The back end needs Apache Pulsar and Redis running, and the provided docker-compose.yml starts them. You first build the base jdk17-curl image, then run sbt docker:publishLocal to publish the service images, then bring the stack up with docker-compose up.

Which of the three ways to build the base Docker image still works?

The README notes that as of September 2022 the image produced by nix build .#docker is no longer compatible with sbt-native-packager. You should either use nix build, which defaults to the slim image, or build directly with the given modules/Dockerfile via docker build -t jdk17-curl modules/.

Why is the feed service not started by docker-compose?

The README recommends running the feed service directly from sbt whenever necessary, because the feed publishes random data to the topics that the other services consume messages from. The compose file starts the other services, Prometheus, Grafana and Pulsar Manager.

How do I build the Elm web application?

With Nix, run nix build .#elm-webapp and open result/index.html, or use the nix develop .#elm shell and compile with elm make src/Main.elm --output=Main.js. The Scala replica of the same app is built with sbt 'webapp/fullLinkJS'.

Official sources

  1. gvolpe/trading on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gvolpe-trading.svg)](https://hysenlabs.com/projects/gvolpe-trading)

Was this useful?