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? :)
Don’t forget to check repo: https://github.com/num8er/codomari-backend
Talk Soon!