# How Caddy's Modular App Architecture Works: A Deep Dive into the Plugin System

> Explore Caddy's modular app architecture and plugin system. Learn how Caddy's functional components implement the caddy.Module interface and register for lazy loading via JSON config.

- Repository: [Caddy/caddy](https://github.com/caddyserver/caddy)
- Tags: deep-dive
- Published: 2026-03-03

---

**Caddy's modular app architecture is a runtime plugin system where every functional component—from HTTP servers to TLS issuers—implements the `caddy.Module` interface, registers via `caddy.RegisterModule()` in an `init()` function, and is lazily loaded from JSON configuration through the `Context.App()` method.**

Caddy is not a monolithic web server. Instead, it is built around a flexible module system that allows developers to extend core functionality without modifying the main codebase. This architecture enables dynamic discovery, instantiation, and wiring of components at runtime, whether you are configuring the built-in HTTP app, a custom storage backend, or a third-party middleware plugin.

## Core Components of the Module System

### Module Definition and Registration

Every Caddy module must implement the `caddy.Module` interface defined in [`modules.go`](https://github.com/caddyserver/caddy/blob/main/modules.go). This interface requires only a single method: `CaddyModule()`, which returns a `caddy.ModuleInfo` struct containing a unique module ID and a constructor function.

In [`modules.go`](https://github.com/caddyserver/caddy/blob/main/modules.go) (lines 29-38), the `ModuleInfo` struct is defined as:

```go
type ModuleInfo struct {
    ID    ModuleID
    New   func() Module
    // ...
}

```

Modules register themselves by calling `caddy.RegisterModule()` inside an `init()` function. As shown in [`modules.go`](https://github.com/caddyserver/caddy/blob/main/modules.go) (lines 124-131), this function stores the `ModuleInfo` in a global map protected by `modulesMu`:

```go
func RegisterModule(m Module) {
    modulesMu.Lock()
    defer modulesMu.Unlock()
    info := m.CaddyModule()
    modules[info.ID] = info
}

```

### Module IDs and Namespaces

Module IDs follow the format `<namespace>.<name>`, parsed in [`modules.go`](https://github.com/caddyserver/caddy/blob/main/modules.go) (lines 85-101). The namespace is everything before the last dot, while the final label represents the module name. Top-level apps use an empty namespace, such as `http` or `tls`, while submodules use nested namespaces like `http.handlers.reverse_proxy`.

## Configuration and App Loading

### The Config Structure

The root configuration object, defined in [`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go) (lines 71-82), uses a `Config` struct that holds raw JSON for each application:

```go
type Config struct {
    AppsRaw ModuleMap `json:"apps,omitempty"`
    StorageRaw json.RawMessage `json:"storage,omitempty"`
    // ...
}

```

`ModuleMap` is a map where keys are module names (e.g., `"http"`) and values are raw JSON messages. This design allows Caddy to defer unmarshaling until the specific app is actually needed.

### Lazy Loading via Context

When Caddy needs to activate an application, it invokes `ctx.App(name)` defined in [`context.go`](https://github.com/caddyserver/caddy/blob/main/context.go) (lines 85-115). This method implements lazy loading:

1. Check if the app already exists in `cfg.apps` (the cache)
2. If missing, retrieve raw JSON from `cfg.AppsRaw[name]`
3. Call `LoadModuleByID(name, raw)` to instantiate the module
4. Store the result in `cfg.apps` and clear the raw JSON entry for GC

This pattern ensures that only required applications consume memory and CPU resources during startup.

### Inline Module Loading

For configurations where the module name is embedded within the object rather than as a map key, [`context.go`](https://github.com/caddyserver/caddy/blob/main/context.go) (lines 71-80) provides `loadModuleInline`. This helper extracts the module identifier from a struct field and loads the concrete type via `LoadModuleByID`, enabling arrays of modules and multiple instances of the same module type with different configurations.

## The Application Lifecycle

### Provisioning

After instantiation, if a module implements the `caddy.Provisioner` interface, Caddy calls its `Provision(ctx)` method. This is where apps perform setup that requires access to other modules or the file system. For example, the HTTP app in [`modules/caddyhttp/app.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/app.go) uses provisioning to set up route handlers and connect to the TLS app via `ctx.App("tls")`.

### Starting and Stopping

All apps must implement the `caddy.App` interface defined in [`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go) (lines 97-101):

```go
type App interface {
    Start() error
    Stop() error
}

```

Once provisioning completes, Caddy invokes `Start()` to begin the application's main operation, such as opening network listeners. During configuration reloads or shutdown, Caddy calls `Stop()` to gracefully terminate the application.

### HTTP App Example

The built-in HTTP server demonstrates this architecture in practice. Located in [`modules/caddyhttp/app.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/app.go), it defines an `App` struct that implements `caddy.Module`, `caddy.Provisioner`, and `caddy.App`. During provisioning, it unmarshals server configurations and sets up the middleware chain. When started, it begins listening on configured ports, and when stopped, it shuts down the HTTP servers gracefully.

## Building a Custom App

You can extend Caddy by creating a custom module that follows the same patterns as built-in apps. Here is a minimal example:

```go
package myapp

import (
	"fmt"
	"github.com/caddyserver/caddy/v2"
)

// Define the app type implementing caddy.App
type MyApp struct {
	Message string `json:"message,omitempty"`
}

// Register the module on import
func init() { caddy.RegisterModule(MyApp{}) }

// Provide module metadata
func (MyApp) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "myapp",
		New: func() caddy.Module { return new(MyApp) },
	}
}

// Validate configuration
func (a *MyApp) Provision(ctx caddy.Context) error {
	if a.Message == "" {
		return fmt.Errorf("message is required")
	}
	return nil
}

// Start the application
func (a *MyApp) Start() error {
	fmt.Println("MyApp started:", a.Message)
	return nil
}

// Stop the application
func (a *MyApp) Stop() error {
	fmt.Println("MyApp stopped")
	return nil
}

```

To use this app, include it in your Caddy JSON configuration:

```json
{
  "apps": {
    "myapp": {
      "message": "Hello from MyApp"
    }
  }
}

```

When Caddy loads this configuration, it automatically executes the registration, loading, provisioning, and startup sequence.

## Summary

- **Caddy's modular app architecture** relies on the `caddy.Module` interface, where each component implements `CaddyModule()` to return metadata including a unique ID and constructor.
- **Registration** occurs in `init()` functions via `caddy.RegisterModule()`, storing module info in a global map in [`modules.go`](https://github.com/caddyserver/caddy/blob/main/modules.go).
- **Configuration** uses a deferred loading pattern where `caddy.Config` holds raw JSON in `AppsRaw`, and `Context.App()` lazily instantiates apps via `LoadModuleByID` in [`context.go`](https://github.com/caddyserver/caddy/blob/main/context.go).
- **Lifecycle** follows three phases: **Provisioning** (setup and validation via `Provisioner` interface), **Starting** (opening resources via `App.Start()`), and **Stopping** (cleanup via `App.Stop()`).
- **Extensibility** allows developers to create custom apps by implementing the same interfaces used by built-in modules like the HTTP app in [`modules/caddyhttp/app.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/app.go).

## Frequently Asked Questions

### What is the minimum interface required to create a Caddy module?

At minimum, a type must implement the `caddy.Module` interface defined in [`modules.go`](https://github.com/caddyserver/caddy/blob/main/modules.go), which requires a single method: `CaddyModule() caddy.ModuleInfo`. This method must return a struct containing a unique `ModuleID` and a `New` constructor function. If you want your module to function as a top-level app, it must also implement the `caddy.App` interface with `Start()` and `Stop()` methods.

### How does Caddy handle module dependencies during configuration loading?

Caddy uses lazy loading through the `Context.App()` method in [`context.go`](https://github.com/caddyserver/caddy/blob/main/context.go). When a module needs to access another app (for example, when the HTTP app needs the TLS app), it calls `ctx.App("tls")`. If the app is not yet loaded, Caddy retrieves the raw JSON from `Config.AppsRaw`, instantiates the module via `LoadModuleByID`, caches it in `cfg.apps`, and then returns it. This ensures dependencies are loaded only when needed and prevents circular initialization issues.

### Can multiple instances of the same module type be used in a single configuration?

Yes, Caddy supports multiple instances through **inline module loading**. While top-level apps use the `AppsRaw` map (where keys are unique), you can embed module configurations directly within other structures using the inline loading mechanism handled by `loadModuleInline` in [`context.go`](https://github.com/caddyserver/caddy/blob/main/context.go). This allows arrays of modules or multiple instances of the same handler type with different configurations, such as multiple reverse proxy upstreams with distinct settings.

### Where does module registration actually happen in the Caddy source code?

Module registration occurs in the `init()` functions of individual module packages, but the central registry logic resides in [`modules.go`](https://github.com/caddyserver/caddy/blob/main/modules.go). The `RegisterModule()` function (lines 124-131) stores module metadata in a global `modules` map protected by `modulesMu`. Additionally, [`modules.go`](https://github.com/caddyserver/caddy/blob/main/modules.go) (often auto-generated or maintained in [`cmd/caddy/main.go`](https://github.com/caddyserver/caddy/blob/main/cmd/caddy/main.go) in older versions) imports all module packages to ensure their `init()` functions execute, populating the registry before the main configuration loads.