Prismatic Platform is a single Elixir umbrella project containing 94 OTP applications. This is not a typical setup. Most Elixir teams work with 3-10 apps. This post shares what we have learned managing an umbrella at this scale.
#Why Umbrella
The umbrella architecture provides three properties that alternatives lack:
Compile-time dependency checking: The Elixir compiler validates that dependencies between apps are declared in mix.exs. If prismatic_web calls a function in prismatic_osint_core, that dependency must be explicit. This prevents accidental coupling.
Independent compilation: Each app compiles independently. When you change prismatic_nabla, only apps that depend on it recompile. In a monolithic project, any change recompiles everything.
Release granularity: You can build releases containing specific subsets of apps. A worker node might include only prismatic_osint_sources and prismatic_storage_ecto, excluding the entire web interface.
#The 94-App Landscape
Our apps fall into natural layers β hover any bar for representative apps:
#Dependency Management at Scale
The most critical discipline in a large umbrella is dependency hygiene. Every app declares its dependencies explicitly:
# apps/prismatic_dd/mix.exs
defp deps do
[
{:prismatic, in_umbrella: true},
{:prismatic_storage_ecto, in_umbrella: true},
{:prismatic_nabla, in_umbrella: true},
{:prismatic_osint_core, in_umbrella: true},
# External dependencies
{:ecto_sql, "~> 3.11"},
{:jason, "~> 1.4"}
]
endRules we enforce:
- No circular dependencies β the compiler catches these, but they indicate architectural problems
- Layer discipline β Layer 1 apps never depend on Layer 3+. Storage apps never depend on domain apps.
- Explicit over implicit β if you use a module from another app, declare the dependency
- Version constraints on all external deps β no floating versions, no git dependencies pointing at branches
#Compilation Strategy
Full compilation of 94 apps takes time. We use several strategies to keep development fast:
Focused compilation: During development, compile only the app you are working on:
cd apps/prismatic_dd && mix compileParallel compilation: Elixir compiles files in parallel within each app and compiles independent apps in parallel.
Incremental compilation: The compiler tracks file dependencies and only recompiles what changed. This works well until you change a shared type or behaviour, which triggers cascading recompilation.
CI compilation: In CI, we compile with --warnings-as-errors to catch all issues:
mix compile --warnings-as-errors --force#Testing Patterns
With 94 apps, running the full test suite takes significant time. Our approach:
Per-app testing: Most development uses per-app tests:
cd apps/prismatic_dd && mix testChanged-app testing: CI runs tests only for apps that changed (based on git diff) plus their dependents.
Integration tagging: Cross-app tests are tagged:
@tag :integration
test "DD pipeline processes OSINT results" do
# This test touches prismatic_dd + prismatic_osint_core
endAsync test isolation: Tests that touch the database use async: false to prevent pool contention. Pure unit tests use async: true.
#When Umbrella Breaks Down
The umbrella architecture has limitations:
Shared configuration: All apps share the same config/ directory. This means every appβs configuration is loaded at startup, even if the app is not started. We mitigate this with runtime configuration:
# config/runtime.exs
if config_env() == :prod do
config :prismatic_storage_ecto, PrismaticStorageEcto.Repo,
url: System.get_env("DATABASE_URL")
endMix task namespace collisions: Two apps cannot define the same mix task name. We prefix all custom tasks with their app name.
Release complexity: Building a release with 94 apps requires careful configuration of which apps start automatically and which are loaded on demand.
IDE performance: Language servers and code intelligence tools can struggle with 94 apps. We use .formatter.exs at the root level and app-specific .credo.exs files.
#Lessons Learned
After 18 months with this architecture:
- Start with fewer, larger apps β split when you have a clear boundary, not preemptively
- Enforce layer discipline from day one β it is nearly impossible to untangle later
- Invest in per-app testing β the full suite is for CI, not local development
- Document dependency decisions β why does app X depend on app Y?
- Monitor compilation times β if full compile exceeds 2 minutes, investigate dependency chains
The umbrella architecture is not for every project. It shines when you have genuine domain boundaries, shared infrastructure, and a team that understands OTP supervision. For most Elixir projects, a single app with well-organized contexts is sufficient.
Learn more about our architecture at Architecture Documentation or explore the Developer Portal for contribution guidelines.