Architecture

Caddy Architecture: How It Actually Works

caddyserver/caddy Apache-2.0 1 diagram
How to read this page

Symvanta parsed this repository into a code graph: every function, class, and method is a node, and every call or import between them is an edge. Everything on this page is computed from that graph at the commit shown above. The terms:

Module (or cluster)
A group of symbols that call each other far more than they call anything else. An algorithm called Louvain community detection finds these groups from the call traffic alone; nobody draws them by hand.
Modularity (the Q number)
A 0-to-1 score of how cleanly those groups separate. Higher means more call traffic stays inside its own group; scores around 0.7 and above read as clean boundaries.
Hub
The most depended-upon symbol inside one module.
Load-bearing symbols
PageRank, the algorithm Google originally used to rank web pages, run over the call graph instead: it surfaces the functions the rest of the codebase leans on hardest.
Arrows and their numbers
How many calls cross from one module into another. A heavier arrow means tighter coupling between those two parts.
Dependency cycle
File A imports B, which imports A again, sometimes through a longer loop. Cycles are not bugs, but a change inside one tends to ripple around the whole loop.
Mutually recursive symbols
Functions that call each other, usually the natural shape of parsers and tree-walking code.

Caddy is a web server built as a module system: a small core that parses config, loads modules, and swaps configurations at runtime, with the HTTP server, reverse proxy, and TLS automation all plugged in as modules. The graph at 947087c shows that split directly. Symvanta detects 46 functional modules at modularity Q=0.79, and the biggest clusters are the config machinery itself: the Caddyfile dispenser (513 symbols), the placeholder replacer (496), and the module registry and loader (356 and 311).

That is the structural signal: in most servers the request path dominates the map, and in Caddy the config path does. RegisterModule, LoadModule, and the Val token dispenser rank in the PageRank top 10 because every plugin in the repo goes through them. The mutually recursive symbols tell the same story: the largest group is the config-swap cycle in caddy.go (changeConfig, run, provisionContext, finishSettingUp).

Module map

The diagram shows the 10 largest of the 46 detected modules, with edges weighted by how many calls cross between them. The biggest holds 513 symbols and its hub is Val, the token accessor on the Caddyfile dispenser (caddyconfig/caddyfile/dispenser.go): every directive in every plugin reads its arguments through it. Module names are checked by hand against the files their members live in; the symbol counts, hubs, and edge weights are what the graph computed.

caddyserver/caddy module map: the 10 largest of 46 detected modules with call-weighted edges, generated by Symvanta
Module map of caddyserver/caddy, generated by Symvanta. Link to this diagram Open full size

Where to start reading

These are the most depended-upon symbols by PageRank over the call graph. Three entries from the raw ranking are left out below: NewTester and initServer belong to the integration test harness in caddytest, and filtered is a zap field-encoder internal from the log filters. They rank high without pointing anywhere useful.

  • Log
  • Val
  • RegisterModule
  • LoadModule
  • NewContext
  • provisionHeaderAliasAllowlist
  • determineTrustedProxy
  • allTokens

Key subsystems

Caddyfile dispenser

Val, Next, NextArg, and ArgErr on the dispenser (caddyconfig/caddyfile/dispenser.go): the token reader every Caddyfile directive parses itself with. 513 symbols, the largest cluster in the repo, and its edges fan out to the httpcaddyfile adapter, the registry, and the core.

Placeholder replacer

NewReplacer and the {placeholder} substitution engine from replacer.go. 496 symbols. Placeholders reach into every layer, so this cluster talks to ten others, with its heaviest edge into the HTTP server internals.

Core logging and network addresses

The caddy package's shared surface: the Log global logger accessor (logging.go), network address parsing (JoinNetworkAddress, ParseNetworkAddressWithDefaults), and config adapters. 445 symbols.

Module and directive registry

RegisterModule (modules.go) plus RegisterDirective and RegisterHandlerDirective: the init-time registry every plugin registers into. 356 symbols.

LoadModule and provisioning

Context.LoadModule and LoadModuleByID (context.go): the reflection walk that turns raw JSON config into provisioned module instances. 311 symbols, and its heaviest edge runs into the core cluster (61 calls).

Context and metrics

NewContext (context.go:65) with the metrics registry and the instrumented-route wrappers. 210 symbols.

Shared infrastructure

The rest of the top 10: the integration test harness (206 symbols, hub NewTester), HTTP server internals (195, hub provisionHeaderAliasAllowlist from modules/caddyhttp/server.go), the httpcaddyfile adapter (166, hub Name), and the reverse proxy upstreams (164, whose hub resetDynamicHosts is a test helper, which is what a hub looks like when the tests drive every upstream path).

Canonical request flow

The sequence worth reading first is a config apply: what happens when a new configuration reaches the admin API. Take the config lock, decode and diff the JSON, provision a fresh Context, load every module the config references, start the apps, then finish setting up the admin endpoint and config loaders. Every step below is a call edge in the config-swap cycle in caddy.go, in the order the work happens.

  1. changeConfig (caddy.go:158)
  2. unsyncedDecodeAndRun (caddy.go:337)
  3. run (caddy.go:419)
  4. provisionContext (caddy.go:484)
  5. Context.LoadModule
  6. LoadModuleByID
  7. finishSettingUp

Health signals

Symvanta detected 0 dependency cycles across 46 modules (modularity Q=0.79). 6 sets of mutually recursive symbols were also detected, the largest being caddy (9 symbols).

See your own codebase mapped like this.

Book a demo →

Auto-generated by Symvanta from the public repo caddyserver/caddy at commit 947087c , licensed Apache-2.0 .

Machine-readable companion: data.json (module counts, subsystems, load-bearing symbols, health signals).

Get this for your codebase →