Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Modular Design in Elixir (ElixirConf EU 2019)

Modular Design in Elixir (ElixirConf EU 2019)

A talk given at ElixirConf EU 2019 in Prague. It talks about splitting big systems into smaller, simpler, more manageable modules that are easier to understand, test and work with.

Maciej Kaszubowski

April 08, 2019
Tweet

More Decks by Maciej Kaszubowski

Other Decks in Programming

Transcript

  1. mkaszubowski94 ... and we can do the same in a

    monolith with a single PostgreSQL instance
  2. mkaszubowski94 Each module can be: understood in isolation modified in

    isolation tested in isolation replaced or removed
  3. mkaszubowski94 create a new job name image url price publish

    John Alchemist create new job manage jobs find a job logout description date
  4. mkaszubowski94 your jobs John Alchemist create new job manage jobs

    find a job logout my awesome job accept my other job cancel accept cancel yet another one and one more accepted this didn’t go well canceled accept cancel
  5. mkaszubowski94 browse jobs John Alchemist create new job manage jobs

    find a job logout my awesome job my other job yet another one and one more filters 100$ 50$ 3500$ 99$ xxxx yyy zzz search
  6. mkaszubowski94 start this job John Alchemist create new job manage

    jobs find a job logout Job Title I need something done start a job https://example.com/jobs/1234 100$
  7. mkaszubowski94 defmodule MyApp.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  8. mkaszubowski94 start this job John Alchemist create new job manage

    jobs find a job logout my awesome job Don’t ask, you’ll get paid start a job https://example.com/jobs/my-awesome-job
  9. mkaszubowski94 defmodule MyApp.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :price, :integer field :slug, :string field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  10. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug()
  11. mkaszubowski94 create a new job name image url price continue

    John Alchemist create new job manage jobs find a job logout description date
  12. mkaszubowski94 pay for job pay 110$ John Alchemist create new

    job manage jobs find a job logout my awesome job job price………….………..100$ fee (10%)…………………….10$ total……………………………..110$ Don’t ask, you’ll get paid Your job will be published after the payment
  13. mkaszubowski94 defmodule MyApp.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :slug, :string field :paid, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  14. mkaszubowski94 browse jobs John Alchemist create new job manage jobs

    find a job logout my awesome job my other job yet another one and one more filters 100$ 50$ 3500$ 99$ xxxx yyy zzz search Upcoming job Job starting in 54 minutes ! x
  15. mkaszubowski94 defmodule MyApp.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  16. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  17. mkaszubowski94 jobs JobContext User Context users join (jobs_count) Payments JobSubtask

    Context preload jobs Chat Context JOB USER CONVERSATION MESSAGE PAYMENT SUBTASK
  18. mkaszubowski94 In most projects: • few nouns / entities •

    a lot of interactions between them • logic is organised around the nouns • most actions represented by updates • a lot of coupling as a result
  19. mkaszubowski94 defmodule MyApp.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  20. mkaszubowski94 1. what is the desired outcome? 2. what data

    do I need to do this? 3. how can I get this data?
  21. mkaszubowski94 1. what is the desired outcome? 2. what data

    do I need to do this? 3. how can I get this data? 4. what should be exposed in the interface?
  22. mkaszubowski94 start this job John Alchemist create new job manage

    jobs find a job logout my awesome job Don’t ask, you’ll get paid start a job https://example.com/jobs/my-awesome-job
  23. mkaszubowski94 def show(conn, %{"job_id"  slug}) do job = slug

     SeoContext.fetch_id_by_slug("job")  JobContext.fetch_by_id() #  end
  24. mkaszubowski94 defp put_slugs(jobs) do ids = Enum.map(jobs, & &1.id) slugs

    = SeoContext.fetch_slugs_by_ids(ids, "job") Enum.map(jobs, fn job  Map.put(job, :slug, slugs[job.id]) end) end
  25. mkaszubowski94 defmodule MyApp.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  26. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  27. mkaszubowski94 SEO Context • slugs for other resources - easy

    to do • multiple slugs - easy to do • slugs are resolved at the controller level • no slugs in domain code
  28. mkaszubowski94 no slugs in domain logic /api/jobs/my-awesome-job/subtasks from( s in

    Subtask, join: j in Job, on: s.job_id  j.id where: j.id  ^job_id or j.slug  ^job_id, # … ) /api/jobs/1/subtasks
  29. mkaszubowski94 browse jobs John Alchemist create new job manage jobs

    find a job logout my awesome job my other job yet another one and one more filters 100$ 50$ 3500$ 99$ xxxx yyy zzz search Upcoming job Job starting in 54 minutes ! x
  30. mkaszubowski94 defmodule Notifications.Worker do use GenServer def handle_info(:work, state) do

    Notifications.send_notifications(now()) Process.send_after(self(), :work, @interval) {:noreply, state) end end
  31. mkaszubowski94 defmodule Notifications do def send_notifications(datetime) do jobs = JobContext.search(…)

    already_sent = fetch_already_sent() sender = FirebaseAdapter :ok = Logic.send( jobs, datetime, already_sent, sender ) end end
  32. mkaszubowski94 defmodule Notifications.Logic do def send(jobs, datetime, already_sent, sender) do

    Enum.each(jobs, fn job -> if starting_soon?(job) && not_sent_yet?(job, already_sent) do sender.send_notification(job) end end) end end
  33. mkaszubowski94 defmodule MyApp.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  34. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  35. mkaszubowski94 JobContext • adding / publishing jobs • searching for

    available jobs • managing ongoing jobs • archive / history • …
  36. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  37. mkaszubowski94 defmodule JobBoard do defmodule Job do @keys [:name, :description,

    ] @enforce_keys @keys defstruct @keys end def publish(%Job{} = job) do #  end end
  38. mkaszubowski94 Job Publishing drafts Job Board jobs publish enforces job

    structure in params controls the rules for publishing
  39. mkaszubowski94 defmodule MyApp.JobBoard.Job do use Ecto.Schema @schema_prefix “job_board” schema "jobs"

    do field :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  40. mkaszubowski94 JobBoard - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  41. mkaszubowski94 JobBoard - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  42. mkaszubowski94 JobBoard • can be tested in isolation (without mocking

    payments) • logic without conditionals • no risk of showing unpublished jobs
  43. mkaszubowski94 JobPublishing • rules for publishing can be changed in

    isolation • can be used alongside other modules
  44. mkaszubowski94 defmodule MyApp.JobBoard.Job do use Ecto.Schema @schema_prefix “job_board” schema "jobs"

    do field :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  45. mkaszubowski94 JobBoard - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  46. mkaszubowski94 Important decisions • what are the rules for publishing

    jobs? • when/what notifications are sent? • which jobs are visible on the job board? • how is a contractor chosen for each job? • what happens after a job is started? when the job ends? • how payments work? • …
  47. mkaszubowski94 JobContext • no clear responsibility • hard to change

    and understand • hard to test in isolation • grows with each new feature • hard to delete old features
  48. mkaszubowski94 Smaller modules • clear responsibilities for each module •

    easy to change, refactor, understand • easy to test in isolation • tend to stay small • trivial to delete if no longer necessary • written once and then forgotten
  49. mkaszubowski94 Downsides • statistics / reports / admin panels •

    UI has to change sometimes • More initial work • Performance (?) • ...?