Skip to main content

My First Event Sourced Application

·4 mins

TL;DR: I built an event sourced application that shows the latest version of common programming languages. Find it here.

For I while I thought about building an event sourced application. The concept is so different from a classical CRUD approach that I was very intrigued.

I tried once with a podcasting transcription app (the details of that can be read here) but I quickly became overwhelmed by all the design decisions I had to make.

After a while, I stumbled upon an event sourcing library called commanded which already provides a lot of the parts you need for an ES application.

After playing around with it I quickly decided on a problem I wanted to solve for myself:

I want to have an overview of the latest versions of the most common programming languages

Spoiler alert you can find it here: https://releaseping.com

Obviously, I didn’t want a spreadsheet but a system that automatically updates itself.

Choosing an ES application for this use seemed like a good idea, so I got started.

I pretty quickly had a general idea. Most of the programming languages out there have a Github repository (or at least a GitHub mirror) with the versions defined as git tags.

So pretty much all I need to do is poll the tags of these repositories and whenever I find a new version, dispatch it as a command.

But first I need an aggregate and a genesis event to create it:

defmodule AddSoftware do
  defstruct [
    uuid: nil,
    name: nil,
    website: nil,
    github: nil,
    licenses: []
  ]
end

Dispatching this command to the aggregate will create a new piece of software (if not already present):

def execute(%Software{uuid: nil}, %AddSoftware{} = add) do
%SoftwareAdded{
      uuid: add.uuid,
      name: add.name,
      type: add.type,
      website: add.website,
      github: add.github,
      licenses: add.licenses
    }
  end
end

After this a scheduled process will poll all tags from the configured github repository and dispatch PublishRelease commands:

def execute(%Software{} = software, %PublishRelease{} = publish) do
  cond do
    is_nil(publish.version_string) -> nil
    MapSet.member?(software.existing_releases, publish.version_string) -> nil
    true ->
      %ReleasePublished{
        uuid: publish.uuid,
        software_uuid: publish.software_uuid,
        version_string: publish.version_string,
        release_notes_url: publish.release_notes_url,
        display_version: display_version,
        published_at: Conversion.from_iso8601_to_naive_datetime(publish.published_at),
        pre_release: publish.pre_release,
      }
  end
end

Some basic validation (don’t emit event when version has already been captured before or if the version information is nil) and then a new ReleasePublished event is emitted. The field pre_release is a boolean that indicates if the version is a pre_release (alpha, beta, rc etc…).

I have implemented some other events (mainly for correcting previously entered fields) but that is the gist of it.

From these 2 events you can build the projection that makes up the page:

defmodule ReleasePing.Api.Projectors.Software do
  use Commanded.Projections.Ecto, name: "Api.Projectors.Software"

  project %SoftwareAdded{} = added, %{stream_version: stream_version} do
    Ecto.Multi.insert(multi, :software, %Software{
      id: added.uuid,
      stream_version: stream_version,
      name: added.name,
      slug: added.slug,
      website: added.website,
      licenses: Enum.map(added.licenses, &map_license/1),
    })
  end

  project %ReleasePublished{} = published, _metadata do
    existing_software = Repo.get(Software, published.software_uuid)
    existing_stable = existing_software.latest_version_stable
    existing_unstable = existing_software.latest_version_unstable

    version_info = published.version_string

    stable_version_to_set = cond do
      published.pre_release -> existing_stable # published version is not a pre release? no change here
      existing_stable == nil -> new_version # version has not been set before? latest version will be changed
      VersionUtils.compare(new_version, existing_stable) == :gt -> new_version  # version is newer? latest version will be changed
      true -> existing_stable # for everything else, don't change the version
    end

    # same applies for the unstable version, except we ignore the `pre_release` flag
    unstable_version_to_set = cond do
      existing_unstable == nil -> new_version
      VersionUtils.compare(new_version, existing_unstable) == :gt -> new_version
      true -> existing_unstable
    end

    changeset = existing_software
      |> Ecto.Changeset.change()
      |> Ecto.Changeset.put_embed(:latest_version_stable, stable_version_to_set)
      |> Ecto.Changeset.put_embed(:latest_version_unstable, unstable_version_to_set)

    Ecto.Multi.update(multi, :api_software, changeset)
  end
end

There’s more to it, specifically, the polling from Github is a little bit more tricky as not all programming languages follow the same version scheme.

In the end, I had a lot of fun building it and am already thinking of possible features to implement.

Contact me with ideas on where to take this.