Umbrella Architecture at Scale: Managing 94 OTP Applications
Lessons learned from building and maintaining a 94-app Elixir umbrella project. Covers dependency management, compilation strategies, testing patterns, and when umbrella architecture breaks down.
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"}
]
end
Rules 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 compile
Parallel 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 --forceTesting 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 test
Changed-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
end
Async 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")
end
Mix 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.
Dive deeper into the Prismatic Platform -- explore our architecture, agents, and epistemic framework.