Umbrella Application

architecture

Umbrella Application

Definition and Overview

An Umbrella Application is an Elixir/OTP project structure that houses multiple interconnected applications within a single repository under a shared apps/ directory. Each child application maintains its own mix.exs, dependencies, supervision tree, configuration, and test suite while sharing a common build system, dependency resolution, and deployment pipeline. This structure enables modular architecture with clear compile-time boundaries between domains while avoiding the operational complexity of distributed microservices.

The umbrella pattern addresses a fundamental tension in software architecture: the need for both modularity (clear boundaries, independent development, isolated failure) and integration (shared data types, direct function calls, coordinated deployment). Microservices provide modularity but impose distributed systems complexity -- network latency, serialization overhead, service discovery, partial failure handling. Monoliths provide integration but resist modularity -- everything depends on everything, changes in one area affect others, testing requires the entire system. Umbrella applications provide a third option: monorepo modularity with monolith integration.

In an umbrella project, each child application is a first-class OTP application with its own Application module, supervision tree, and process hierarchy. Applications declare dependencies on each other explicitly in their mix.exs files, and the compiler enforces these boundaries -- an application cannot call modules from another application unless it declares the dependency. This compile-time enforcement prevents the gradual erosion of boundaries that plagues monolithic applications, where any module can call any other module without restriction.

The deployment model for umbrella applications differs fundamentally from microservices. All child applications are compiled into a single OTP release and run within a single BEAM virtual machine instance. This means inter-application communication uses direct function calls (microsecond latency) rather than network requests (millisecond latency), applications share the same ETS tables and process registry, and a single deployment unit contains the entire system. The trade-off is that all applications must be deployed together, but the benefit is dramatically simpler operations and orders-of-magnitude lower communication latency.

The Prismatic Platform uses an umbrella structure containing 115 OTP applications, from foundational layers (prismatic_storage_core, prismatic) to domain-specific systems (prismatic_perimeter, prismatic_agents, prismatic_web) to specialized adapters (prismatic_storage_ets, prismatic_storage_ecto, prismatic_storage_meilisearch). This structure supports the platform's 6,652 Elixir source files, 5,864 test files, and enables teams of agents to work on isolated domains without creating conflicts across the codebase.

Historical Context and Motivation

The umbrella pattern in Elixir emerged from the Erlang/OTP tradition of building systems as collections of loosely coupled applications. In Erlang, large systems like the AXD 301 switch were composed of hundreds of OTP applications, each with its own supervision tree and deployment configuration. Elixir's Mix build tool formalized this pattern through the --umbrella flag, providing project generation, dependency management, and build orchestration specifically designed for multi-application projects.

The Prismatic Platform adopted the umbrella structure early in its evolution (Generation 1-3) when it became clear that a monolithic approach would not scale to the platform's ambitions. The initial five applications (prismatic_storage_core, prismatic, prismatic_web, prismatic_storage_ets, prismatic_storage_ecto) established the foundational layers. By Generation 10, the platform had grown to 50+ applications, and by Generation 19, it encompasses 115 applications across multiple domains.

The growth from 5 to 115 applications validated the umbrella decision. Adding a new domain (e.g., prismatic_perimeter for EASM, or prismatic_api for the REST gateway) requires creating a new application directory with its own mix.exs, declaring its dependencies, and implementing its supervision tree. The existing applications are unaffected, and the new application can be developed and tested in isolation before being integrated into the release.

Technical Deep Dive

Umbrella Project Structure

The Prismatic Platform follows the standard Elixir umbrella layout with domain-organized applications:

prismatic-platform/
  mix.exs                    # Root mix.exs (umbrella config)
  mix.lock                   # Shared dependency lock file
  config/
    config.exs               # Shared configuration
    dev.exs                  # Development overrides
    test.exs                 # Test configuration
    prod.exs                 # Production configuration
    runtime.exs              # Runtime configuration
  apps/
    prismatic_storage_core/  # Foundation: traits, protocols, behaviours
      mix.exs                # Own deps: none (leaf application)
      lib/
      test/
      CLAUDE.md              # Application documentation
    prismatic/               # Core: coordination, API facades
      mix.exs                # Deps: prismatic_storage_core, ...
      lib/
      test/
    prismatic_web/           # Web: Phoenix, LiveView
      mix.exs                # Deps: prismatic, phoenix, ...
      lib/
      test/
    prismatic_perimeter/     # EASM: scanning, rating, compliance
      mix.exs                # Deps: prismatic, prismatic_storage_core, ...
      lib/
      test/
    prismatic_api/           # REST API: auto-introspecting gateway
      mix.exs                # Deps: prismatic, open_api_spex, ...
      lib/
      test/
    prismatic_agents/        # Agent runtime: 530 agents
      mix.exs                # Deps: prismatic_storage_core, ...
      lib/
      test/
    ...                      # 109 more applications

Application Dependency Management

Each child application declares its dependencies explicitly, and the compiler enforces these boundaries:

defmodule PrismaticPerimeter.MixProject do
  @moduledoc false
  use Mix.Project

  def project do
    [
      app: :prismatic_perimeter,
      version: "0.1.0",
      build_path: "../../_build",
      config_path: "../../config/config.exs",
      deps_path: "../../deps",
      lockfile: "../../mix.lock",
      elixir: "~> 1.19",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      test_coverage: [tool: ExCoveralls],
      dialyzer: [plt_add_apps: [:ex_unit]],
      compilers: Mix.compilers(),
      aliases: aliases()
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mod: {PrismaticPerimeter.Application, []}
    ]
  end

  defp deps do
    [
      # Umbrella dependencies (compile-time enforced)
      {:prismatic_storage_core, in_umbrella: true},
      {:prismatic, in_umbrella: true},

      # External dependencies
      {:jason, "~> 1.4"},
      {:req, "~> 0.5"},
      {:telemetry, "~> 1.2"}
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  defp aliases do
    [
      quality: ["compile --warnings-as-errors", "credo --strict", "dialyzer"]
    ]
  end
end

The in_umbrella: true flag instructs Mix to resolve the dependency from the local apps/ directory rather than Hex. This provides the same compile-time dependency enforcement as external packages but with local resolution.

Per-Application Supervision Trees

Each umbrella application defines its own supervision tree, creating an isolated process hierarchy:

defmodule PrismaticPerimeter.Application do
  @moduledoc """
  OTP Application for Prismatic Perimeter EASM.
  Defines the supervision tree for scanning, rating, and compliance.
  Uses rest_for_one strategy because downstream processes depend
  on the asset store and rating cache.
  """
  use Application

  @impl Application
  def start(_type, _args) do
    children = [
      # Core data stores (must start first)
      PrismaticPerimeter.AssetStore,
      PrismaticPerimeter.RatingCache,

      # Scanning subsystem (depends on stores)
      {PrismaticPerimeter.Scanner.Supervisor, []},

      # Rating engine (depends on stores and scanner)
      PrismaticPerimeter.Rating.Engine,

      # Compliance assessor (depends on rating engine)
      PrismaticPerimeter.Compliance.Assessor,

      # Task supervisor for parallel operations
      {Task.Supervisor, name: PrismaticPerimeter.TaskSupervisor}
    ]

    opts = [strategy: :rest_for_one, name: PrismaticPerimeter.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Compile-Time Boundary Enforcement

The compiler prevents unauthorized cross-application dependencies. This is a structural guarantee, not a convention:

defmodule PrismaticPerimeter.Example do
  @moduledoc false

  # This works: prismatic_storage_core is a declared dependency
  alias PrismaticStorageCore.Traits.StorageAdapter

  # This works: prismatic is a declared dependency
  alias Prismatic.Coordinator

  # This would FAIL at compile time if prismatic_web is NOT
  # listed in deps. The compiler enforces the boundary.
  # alias PrismaticWeb.Router  # => CompileError
end

This enforcement is what distinguishes umbrella applications from a monolith organized into directories. In a typical monolith, any module can import any other module without restriction. In an umbrella, the dependency declarations create a compile-time contract that the toolchain enforces automatically.

Dependency Graph

The platform's dependency graph forms a directed acyclic graph (DAG) with clear layering:

Layer 0 (no deps):     prismatic_storage_core
                              |
Layer 1 (core deps):   prismatic_storage_ets    prismatic_storage_ecto
                       prismatic_storage_meilisearch    prismatic_types
                              |
Layer 2 (platform):    prismatic    prismatic_agents    prismatic_safety
                       prismatic_supervisor
                              |
Layer 3 (domain):      prismatic_perimeter    prismatic_visitor_intelligence
                       prismatic_claude    prismatic_dark
                              |
Layer 4 (interface):   prismatic_web    prismatic_api

Application Categories

The 115 umbrella applications fall into distinct architectural categories:

CategoryCountExamplesRole
Foundation5prismatic_storage_core, prismatic_typesShared traits, protocols, types
Storage Adapters7prismatic_storage_ets, prismatic_storage_ectoData persistence implementations
Core Platform10prismatic, prismatic_agents, prismatic_safetyPlatform coordination and quality
Domain Applications50+prismatic_perimeter, prismatic_claudeBusiness logic domains
Web/API3prismatic_web, prismatic_apiUser-facing interfaces
Testing/Tooling15+prismatic_test_helpers, prismatic_benchmarksDevelopment infrastructure
Security10+prismatic_dark, Color Team implementationsSecurity operations
Ecosystem5+OSS packages, plugin systemsOpen-source components

Cross-Application Communication Patterns

All inter-application communication occurs within a single BEAM instance, providing microsecond-level latency:

PatternMechanismLatencyUse Case
Direct function callModule.function(args)MicrosecondsSynchronous queries
GenServer callGenServer.call(name, msg)MicrosecondsStateful operations
PubSub broadcastPhoenix.PubSub.broadcast/3MicrosecondsEvent notification
Telemetry events:telemetry.execute/3MicrosecondsObservability
Task.asyncTask.async(fn -> ... end)MicrosecondsParallel computation
Outbox PatternDatabase + publisherMillisecondsReliable cross-app events

Build and Test Isolation

The umbrella structure supports both whole-system and per-application operations:

# Compile all applications
mix compile

# Compile a single application (with dependencies)
mix compile --app prismatic_perimeter

# Run all tests across all applications
mix test

# Run tests for a single application
mix test apps/prismatic_perimeter/test/

# Run quality checks across all applications
mix quality.gates

# Run quality checks for a single application
cd apps/prismatic_perimeter && mix quality

# Check dependency graph for cycles
mix xref graph --format=cycles

# Visualize dependency graph
mix xref graph --format=dot | dot -Tpng -o deps.png

Creating a New Umbrella Application

Adding a new domain to the platform follows a standardized process:

# Generate new application with supervision tree
cd apps
mix new prismatic_new_domain --sup

# The generated structure:
# apps/prismatic_new_domain/
#   mix.exs
#   lib/
#     prismatic_new_domain.ex
#     prismatic_new_domain/application.ex
#   test/
#     prismatic_new_domain_test.exs
#     test_helper.exs

After generation, the application needs configuration:

# 1. Configure umbrella paths in mix.exs
# 2. Add in_umbrella dependencies
# 3. Define Application module with supervision tree
# 4. Create CLAUDE.md documentation
# 5. Add .claude/quality-dna/current-state.json
# 6. Configure quality gates (warnings_as_errors, dialyzer, etc.)

Inter-Application Usage Patterns

# From prismatic_web, calling prismatic_perimeter (direct function call)
defmodule PrismaticWeb.PerimeterLive.Dashboard do
  @moduledoc """
  LiveView dashboard for EASM security ratings.
  Calls PrismaticPerimeter directly for rating data.
  """
  use PrismaticWeb, :live_view

  @impl Phoenix.LiveView
  def mount(_params, _session, socket) do
    {:ok, rating} = PrismaticPerimeter.security_rating("example.com")
    {:ok, assign(socket, rating: rating)}
  end
end

# From prismatic_api, using generic dispatch across umbrella apps
defmodule PrismaticApi.DispatchController do
  @moduledoc """
  Generic API controller that dispatches to any Prismatic facade module.
  Auto-discovered at boot time through module introspection.
  """

  @spec dispatch(Plug.Conn.t(), map()) :: Plug.Conn.t()
  def dispatch(conn, %{"app" => app, "action" => action} = params) do
    case Prismatic.dispatch(app, action, params) do
      {:ok, result} -> json(conn, %{status: "ok", data: result})
      {:error, reason} -> json(conn, %{status: "error", reason: inspect(reason)})
    end
  end
end

Comparison with Alternative Architectures

ArchitectureModularityIntegrationDeploymentCommunicationComplexity
UmbrellaCompile-time boundariesDirect function callsSingle releaseMicrosecondsModerate
MonolithDirectory conventionsDirect function callsSingle deployMicrosecondsLow
MicroservicesService boundariesHTTP/gRPC/messagingIndependentMillisecondsHigh
Monorepo (multi-lang)Language boundariesAPI contractsMixedVariableHigh
Modular MonolithPackage/module boundariesDirect callsSingle deployMicrosecondsModerate

Best Practices

Enforce strict dependency direction. Dependencies should flow from higher layers (web, API) toward lower layers (core, storage). Never create circular dependencies between applications. Use mix xref graph --format=cycles to verify the dependency DAG regularly.

Keep foundation applications dependency-free. Applications like prismatic_storage_core that define traits and protocols should have zero umbrella dependencies. This ensures they can be compiled first and used by all other applications without creating bottlenecks.

One supervision tree per application. Each application should define its own Application module with a supervision tree. Do not rely on parent applications to supervise your processes.

Test applications in isolation. Each application's test suite should run independently. Cross-application integration tests belong in a dedicated test application or the root test suite.

Document application boundaries in CLAUDE.md. Every application should have a CLAUDE.md file that describes its purpose, public API, dependencies, and architectural constraints. This serves as the primary documentation for agent-driven development.

Namespace configuration properly. Use config :prismatic_perimeter, key: value rather than generic keys. Configuration key collisions are a common source of subtle bugs in umbrella projects.

Define clear public API modules. Each application should have a single public facade module (e.g., PrismaticPerimeter) that exposes its public API. Internal modules should use @moduledoc false to signal they are not part of the public contract.

Common Pitfalls

Circular dependencies between applications: Application A depends on B and B depends on A. The compiler will reject this, but cycles involving three or more applications can be subtle. Use mix xref graph --format=cycles to detect them.

Leaking internal modules across boundaries: Applications that expose internal implementation details force consumers to depend on unstable interfaces. Define clear public API modules and use @moduledoc false for internal modules.

Shared configuration confusion: Umbrella applications share config/config.exs, which can lead to configuration key collisions. Always namespace configuration under the application name.

Testing with cross-application side effects: Tests in one application that depend on processes or state from another application create fragile, order-dependent test suites. Mock or stub cross-application dependencies in tests.

Deploying individual applications independently: While umbrella applications can be compiled independently, they are designed to be deployed together as a single OTP release. Attempting to deploy individual applications creates version skew.

Growing applications instead of splitting them: When an application exceeds 50-100 modules, it likely contains multiple distinct concerns that should be separate applications. Split proactively rather than letting applications become internal monoliths.

  • OTP -- The application framework underlying the umbrella structure
  • Supervision Tree -- Per-app process management hierarchies
  • Adapter Pattern -- Cross-app storage abstraction layer
  • Phoenix LiveView -- Web framework application within the umbrella
  • Elixir -- The language providing umbrella project tooling via Mix
  • BEAM -- Virtual machine hosting all umbrella applications in one instance
  • GenServer -- Process abstraction used across all umbrella applications
  • ETS -- Shared in-memory storage accessible across applications
  • Supervisor -- OTP behavior managing processes within each application
  • Quality Debt -- Quality tracking applied per-application

See Also

  • Architecture -- Platform architecture overview
  • Technologies -- Technology stack details
  • Apps -- Application directory with all 115 umbrella applications

Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL