codomari · monorepo · umbrella

Codomari #8 (code organizing / monorepo)

A monorepo (short for “monolithic repository”) is a code organisation approach where multiple cross-dependent apps are stored within a single repository. Think about one git repository where You have apps folder and codomari_api, codomari_workers, codomari_mail_service and etc.

In case of monolythic application we have 1 app and all features, functionalities are used and launched by one app.

In case of micro-services we have multiple apps each responsible for small “by idea” independent area. If you worked in a startups - people mostly jump to write micro-services (because of “it’s cool”) and create per repo per service.

But there comes code duplication - microservices “copy-paste” same code chunks cause they must be “independent”.

Again there comes multiple requests from one service to another through network, we are growing and having network latencies which slows system work.

What if one service could call other service function without doing network requests?

What if services shared code chunks and reused it - by creating “one place to fix”.

That’s why monorepo exists, we want microservices, but we want less network communication, code sharing.


Asked ChatGPT to give description about monorepo:

Key Features of Monorepo:

    Single Source of Truth: All projects and dependencies are stored in one repository.
    
    Shared Code and Dependencies: Allows sharing of code and libraries across projects easily.

    Unified Versioning: All projects can share the same versioning scheme.

    Atomic Changes: Developers can make changes across multiple projects in a single commit.


Benefits:

    Simplified Dependency Management: Shared libraries or utilities can be updated in one place.

    Consistency: Ensures all projects follow the same standards, tools, and configurations.

    Streamlined Collaboration: Teams can see changes across related projects without switching repositories.

    Ease of Refactoring: Changes spanning multiple projects are easier to manage.


The monorepo pattern is widely used by companies like Google, Facebook, and Microsoft to manage their large codebases efficiently.

Based on experience with NestJS framework which makes super easy to create monorepo I decided to make it same way and organized files and folders by same idea:


├── apps
│   └── codomari_api
│       ├── lib
│       ├── priv
│       ├── test
│       └── mix.exs
├── config
├── lib
│   └── codomari
│       ├── lib
│       │   ├── database
│       │   ├── entities
│       │   ├── repositories
│       │   └── services
│       ├── test
│       └── mix.exs
└── mix.exs

By googling for best practices I found Umbrella, Poncho, Workspace solutions, some in forums pointed to own one with long discussions about pros-cons and list continues.

In Discord channel I was told that what I’ve done is modification of Umbrella and in such case lib should not exists, all stuff must be in apps cause mix tool works with apps.

I know I’ll be cursed, but I’ve done based on Umbrella app example, but it’s kinda umbrella where apps use shared dependencies from top level lib.


So we have mix.exs at root which is kinda “stolen” from umbrella project example.

You can see apps_path in project section which tells to mix tool we have many apps in provided folder.

deps section holds only one stuff for now which is common for overall project.

Nothing’s special and magical here :)

defmodule CodomariBackend.MixProject do
  use Mix.Project

  def project do
    [
      apps_path: "apps",    # <= THIS ONE
      version: "0.0.2",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      aliases: aliases(),
      elixirc_paths: elixirc_paths(Mix.env())
    ]
  end

  defp deps do
    [
      # linter
      {:credo, "~> 1.7"},
    ]
  end

  defp elixirc_paths(:test), do: []
  defp elixirc_paths(_), do: []

  defp aliases do
    [
      setup: ["cmd mix setup"]
    ]
  end
end

Same simplicity we can see in lib/codomari/mix.exs.

Same standard project section which imports Codomari module to get name and version of app.

Nothing fancy here too :)

defmodule Codomari.MixProject do
  @moduledoc false

  use Mix.Project

  def project do
    Code.require_file("lib/codomari.ex")
    manifest = Codomari.manifest()

    [
      name: manifest[:name],
      version: manifest[:version],
      type: :library,
      elixir: "~> 1.14",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      aliases: aliases(),
    ]
  end

  defp deps do
    [
      # database and pooling
      {:couchbeam, "~> 1.5.3"},
      {:poolboy, "~> 1.5.2"},

      # fixes issue in Couchbeam
      # https://github.com/benoitc/couchbeam/issues/200
      {:jsx, "2.8.3", override: true}
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test"]
  defp elixirc_paths(_), do: ["lib"]

  defp aliases do
    [
      setup: ["cmd mix setup"]
    ]
  end
end

So now time comes how to import the lib in apps?

Cause simply using Codomari.Database.PoolWorker will end with undefined module issues.

To achieve it we have add tuple in deps with path: keyword pointing to our library:

...
{:codomari, path: "../../lib/codomari"},
...

And here is apps/codomari_api/mix.exs

defmodule CodomariApi.MixProject do
  use Mix.Project

  def project do
    Code.require_file("../../lib/codomari/lib/codomari.ex")
    Code.require_file("lib/codomari_api.ex")
    manifest = CodomariApi.manifest()

    [
      app: manifest[:app],
      version: manifest[:version],
      type: :application,
      build_path: "../../_build",
      config_path: "../../config/config.exs",
      deps_path: "../../deps",
      lockfile: "../../mix.lock",
      elixir: "~> 1.14",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps(),
      releases: releases()
    ]
  end

  def application do
    [
      mod: {CodomariApi.Application, []},
      extra_applications: [:logger, :runtime_tools]
    ]
  end

  def releases do
    [
      codomari_api: [
        include_executables_for: [:unix],
        applications: [
          runtime_tools: :permanent,
          telemetry_metrics: :permanent,
          telemetry_poller: :permanent
        ],
        steps: [
          :assemble,
          :tar
        ]
      ]
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  defp deps do
    [
      {:codomari, path: "../../lib/codomari"},   # <= THIS ONE

      # web framework and related deps
      {:plug_cowboy, "~> 2.5"},
      {:phoenix, "~> 1.7.9"},
      {:phoenix_html, "~> 3.3"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:phoenix_live_view, "~> 0.20.1"},
      {:phoenix_live_dashboard, "~> 0.8.2"},

      # other necessary deps
      {:floki, ">= 0.30.0", only: :test},
      {:swoosh, "~> 1.3"},
      {:finch, "~> 0.13"},
      {:telemetry_metrics, "~> 0.6"},
      {:telemetry_poller, "~> 1.0"},
      {:gettext, "~> 0.20"},
      {:jason, "~> 1.2"},
      {:dns_cluster, "~> 0.1.1"}
    ]
  end

  defp aliases do
    [
      setup: ["deps.get"]
    ]
  end
end

Still nothing specific and magical! Isn’t it? :)


fin


Don’t forget to check repo: https://github.com/num8er/codomari-backend


Talk Soon!