Umbrella Architecture at Scale: Managing 94 OTP Applications - Prismatic Platform
Engineering

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.

Mar 03, 2026 Β· 11 min read Β· Tomas Korcak (korczis)

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:

  1. No circular dependencies – the compiler catches these, but they indicate architectural problems
  2. Layer discipline – Layer 1 apps never depend on Layer 3+. Storage apps never depend on domain apps.
  3. Explicit over implicit – if you use a module from another app, declare the dependency
  4. 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 --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 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:

  1. Start with fewer, larger apps – split when you have a clear boundary, not preemptively
  2. Enforce layer discipline from day one – it is nearly impossible to untangle later
  3. Invest in per-app testing – the full suite is for CI, not local development
  4. Document dependency decisions – why does app X depend on app Y?
  5. 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.

Browse all β†’