Elixir. Phoenix. Domain Driven Design

Mafinar Khan

Software Developer, Tread Inc

In this Talk

  • Some History
  • Some Definitions
  • Some Examples
  • One Proposition

A bit of history... 2009

Asset Tracking System

  • Realtime Location Tracking
  • Permissions Hierarchy
  • Geofence
  • Reports
  • Public API
  • Hiring/Subscription
  • Fleet Management
  • Anti-theft
  • Driving Behavior

Django (MVC.. MTV?)

  • Apps encompassing use cases
  • Active Records for... eh, Domains?
  • Views (Controllers) making the (Fat) model queries/writes
  • Templates Rendering it

Pass 2

  • Move UI to a SPA (React wasn't invented yet)
  • API (Piston, DRF wasn't hot yet)
  • Models are still Fat, controllers are still thin

Uh-oh

  • Requirements pouring in
  • Unplanned for features
  • Lots of changes... everywhere
  • Domain == Models
  • but Domain != Models (What is a Model?)

communication(business, with: code)

Domain Driven Design

  • Bounded Context
  • Domain Entities
  • Value Objects
  • Service Objects
  • Aggregate Roots
  • Event Sourcing

Ubiquitous Language

  • Common Vocabulary between all parties
  • The Verb/Noun Game
  • Facade talking to the nitty-gritty
    • Less contextually costly changes
    • (Eventually) Less Frequent changes

Elixir

  • Functional/Immutable
  • Optionally Types
  • Pattern Matching
  • OTP
  • GenStage

Phoenix (> 1.3)

  • Ecto for Repository
  • Context
  • OTP
  • Thin(ner) Controller
  • Separation of (Domain) Concerns
  • Makes you THINK ABOUT ARCHITECTURE

Before

After

  defp locations(conn, 
    %{"id": id, "lat": lat, "lng": lng) do
    tablename = from(
        v in "vehicle", 
        select: [:tablename], Repo.get()
    )
    query =
      from(
        p in tablename,
        select: %{
          name: p.name,
          distance:
            fragment(
              "ST_Distance(
                the_geom, 
                ST_MakePoint(?, ?)::geography)",
              ^lng,
              ^lat
            )
        },
        where: not is_nil(p.name),
        order_by: [asc: 2],
        limit: ^limit
      )

    locations = query |> Repo.all()

    render(conn, "locations.json", locations: locations)
  end
def location(conn, %{"id": id}, _current_user) do
   locations = id 
    |> LocationService.get_vehicle() 
    |> LocationService.get_locations_for()
   render(conn, "locations.json", locations: locations) 
end

Phoenix Contexts

  • Bounded Contexts
  • Concerns with Services
  • Maps easily with Ubiquitous language
  • Preferably the only thing the controller sees
  • Middleware between Repository and View
  • Can be topic of discussion in those Product X Business jams

Schema

Domain

defmodule Wltx.DB.Vehicle do
  @moduledoc """
  Schema for the vehicle/asset table
  """
  use Ecto.Schema

  schema "asset_vehicle" do
    field(:license_plate, :string)
    field(:vehicle_type_id, :integer)
    field(:group_id, :integer)
    field(:tag_id, :integer)
    field(:code, :string)
    field(:model, :string)
    field(:color, :string)
    field(:device_id, :integer)
    field(:tablename
, :string)
  end
end
defmodule Wltx.Vehicle do
  defstruct 
    license_plate: "", 
    attributes: %AssetAttributes{}, 
    related_table: "",
    group: %Group{},
    device: %Device{},
    can_be_tracked: false,
    is_whitelabelled: false 
end

Aggregates?

  • Hello, GenServer!
  • Supervisor manages (Aggregate Roots)
  • Connect multiple Domain Models/Schema?

Event Sourcing/CQRS

  • What constitutes a state changes
  • How do you see the results
  • GPS Device |> TCPServer |> Parse |> Database |> ReportsOrRealTimeOrGeoFence
  • Models better communicated via events/incidents
  • Writers/Readers
  • Actor Model Anyone?

The Hack Proposition

Tasks

  • Discuss
  • Decide on Structure
  • Write Generators
  • Hack!

Let's Hack!!!

Elixir, Phoenix and DDD

By Mafinar Khan

Elixir, Phoenix and DDD

  • 2,853