Hey yall, I have a project organization question:
We are building two internal elixir apps (i’ll call them “local_app” and “cloud_app”), which share a ton of code so we decided to go with a monorepo strat.
Here are all the elixir projects under the repo. (Note: only the _app
projects are elixir applications. the other projects are simply changesets and functions.)
home
├──local_app
├──cloud_app
├──shared_models
├──local_models_api
└──cloud_models_api
Basically, what happens is that the we needed both the local_app and cloud_app to know what a shared_model is, so we abstracted away those schemas and changesets to the “shared_models” project, which both _app’s declare as a dependency using the path: ../shared_models
option.
We need both apps to know how to read those models, however only one app should have right access to a specific model. For example, we might have a SharedModel.Note
schema, which both apps will know how to read, and the API to get the notes might live there too (SharedModels.Notes.list_notes/0
). However, we only want the local_app to have write privileges to those notes—this is so important that we don’t even want the write functions available to cloud_app at compile time—so we made a local_models_api
project that exposes functions like LocalModelsAPI.Notes.create_note/1
and only the local_app uses that as a dependency.
(The reason we needed that code to live in a separate local_models_api
project and not just live in the local_app
project is because we needed to write test helpers for the cloud_app
test suite that writes a handful of those models in order to test some other behaviors, so we have cloud_app
declaring local_models_api
and a test only dependency.)
Vis-versa, we only have some models that we want write access to via the cloud_app, so we created the cloud_models_api
project.
This leads to messy alias APIs in our application code, for example, its really common for a server_app controller to have
use LocalWeb, :controller
alias LocalModelsAPI.Notes
alias SharedModels.Notes.Note
and we use the defdelegate
pattern at the bottom of the LocalModelsAPI.Notes module to bring in all the SharedModels.Notes functions into the LocalModelsAPI.Notes namespace (which leads to compile time warnings that functions don’t exist, but we ignore those).
It also leads us to have a messy test pattern. For example, we want to test our LocalModelsAPI logic in our local_models_api project, but because it doesn’t have its own stand alone application, it cannot run a DB. So instead, we added the"../local_models_api/test"
to the test_paths
list in our LocalApp mix file. (Which then leads to doubling our tests for the SharedModels project, but that’s fine.)
Is there a simpler way?
To recap, the problems we need to solve:
- Have a shared library of models
- Have some APIs of those models only available to specific projects
- Have all those APIs available to all the projects test helpers