Codementor Events

My First Event Sourced Application

Published Jan 09, 2018Last updated Jul 07, 2018
My First Event Sourced Application

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.

Discover and read more posts from Leif Gensert
get started
post commentsBe the first to share your opinion
Show more replies