Web server: functions to handle different routes sharing all the runtime state of the web application, such as a session store

In-memory cache: functions to get and set data in a shared mutable reference such as a Clojure Atom or Ref

A component is similar in spirit to the definition of an object in Object-Oriented Programming. This does not alter the primacy of pure functions and immutable data structures in Clojure as a language. Most functions are just functions, and most data are just data. Components are intended to help manage stateful resources within a functional paradigm.

Advantages of the Component Model

Large applications often consist of many stateful processes which must be started and stopped in a particular order. The component model makes those relationships explicit and declarative, instead of implicit in imperative code.

Components provide some basic guidance for structuring a Clojure application, with boundaries between different parts of a system. Components offer some encapsulation, in the sense of grouping together related entities. Each component receives references only to the things it needs, avoiding unnecessary shared state. Instead of reaching through multiple levels of nested maps, a component can have everything it needs at most one map lookup away.

Instead of having mutable state (atoms, refs, etc.) scattered throughout different namespaces, all the stateful parts of an application can be gathered together. In some cases, using components may eliminate the need for mutable references altogether, for example to store the “current” connection to a resource such as a database. At the same time, having all state reachable via a single “system” object makes it easy to reach in and inspect any part of the application from the REPL.

The component dependency model makes it easy to swap in “stub” or “mock” implementations of a component for testing purposes, without relying on time-dependent constructs, such as with-redefs or binding, which are often subject to race conditions in multi-threaded code.

Having a coherent way to set up and tear down all the state associated with an application enables rapid development cycles without restarting the JVM. It can also make unit tests faster and more independent, since the cost of creating and starting a system is low enough that every test can create a new instance of the system.

Disadvantages of the Component Model

First and foremost, this framework works best when all parts of an application follow the same pattern. It is not easy to retrofit the component model to an existing application without major refactoring.

In particular, the ‘component’ library assumes that all application state is passed as arguments to the functions that use it. As a result, this framework may be awkward to use with code which relies on global or singleton references.

For small applications, declaring the dependency relationships among components may actually be more work than manually starting all the components in the correct order. You can still use the ‘Lifecycle’ protocol without using the dependency-injection features, but the added value of ‘component’ in that case is small.

The “system object” produced by this framework is a large and complex map with a lot of duplication. The same component may appear in multiple places in the map. The actual memory cost of this duplication is negligible due to persistent data structures, but the system map is typically too large to inspect visually.

You must explicitly specify all the dependency relationships among components: the code cannot discover these relationships automatically.

Finally, the ‘component’ library forbids cyclic dependencies among components. I believe that cyclic dependencies usually indicate architectural flaws and can be eliminated by restructuring the application. In the rare case where a cyclic dependency cannot be avoided, you can use mutable references to manage it, but this is outside the scope of ‘component’.

Define other components in terms of the components on which they depend.

(defrecord ExampleComponent [options cache database scheduler]
component/Lifecycle
(start [this]
(println ";; Starting ExampleComponent")
;; In the 'start' method, a component may assume that its
;; dependencies are available and have already been started.
(assoc this :admin (get-user database "admin")))
(stop [this]
(println ";; Stopping ExampleComponent")
;; Likewise, in the 'stop' method, a component may assume that its
;; dependencies will not be stopped until AFTER it is stopped.
this))

Do not pass component dependencies in a constructor. Systems are responsible for injecting runtime dependencies into the components they contain: see the next section.

Systems

Components are composed into systems. A system is a component which knows how to start and stop other components. It is also responsible for injecting dependencies into the components which need them.

The easiest way to create a system is with the system-map function, which takes a series of key/value pairs just like the hash-map or array-map constructors. Keys in the system map are keywords. Values in the system map are instances of components, usually records or maps.

If the component and the system use different keys, then specify them as a map of {:component-key :system-key}. That is, the using keys match the keys in the component, the values match keys in the system.

Stop a system by calling the stop method on it. This will stop each component, in reverse dependency order, and then re-assoc the dependencies of each component. Note:stop is not the exact inverse of start; component dependencies will still be associated.

It doesn’t matter when you associate dependency metadata on a component, as long as it happens before you call start. If you know the names of all the components in your system in advance, you could choose to add the metadata in the component’s constructor:

Entry Points in Production

The ‘component’ library does not dictate how you store the system map or use the components it contains. That’s up to you.

The typical approach differs in development and production:

In production, the system map is ephemeral. It is used to start all the components running, then it is discarded.

When your application starts, for example in a main function, construct an instance of the system and call component/start on it. Then hand off control to one or more components that represent the “entry points” of your application.

For example, you might have a web server component that starts listening for HTTP requests, or an event loop component that waits for input. Each of these components can create one or more threads in its Lifecycle start method. Then main could be as trivial as:

(defn main [] (component/start (new-system)))

Note: You will still need to keep the main thread of your application running to prevent the JVM from shutting down. One way is to block the main thread waiting for some signal to shut down; another way is to Thread/join the main thread to one of your components’ threads.

Web Applications

Many Clojure web frameworks and tutorials are designed around an assumption that a “handler” function exists as a global defn, without any context. With this assumption, there is no easy way to use any application-level context in the handler without making it also a global def.

The ‘component’ approach assumes that any “handler” function receives its state/context as an argument, without depending on any global state.

To reconcile these two approaches, create the “handler” function as a closure over one or more components in a Lifecycle start method. Pass this closure to the web framework as the “handler”.

Most web frameworks or libraries that have a static defroutes or similar macro will provide an equivalent non-static routes which can be used to create a closure.

More Advanced Usage

Errors

While starting/stopping a system, if any component’s start or stop method throws an exception, the start-system or stop-system function will catch and wrap it in an ex-info exception with the following keys in its ex-data map:

:system is the current system, including all the components which have already been started.

:component is the component which caused the exception, with its dependencies already assoc’d in.

The original exception which the component threw is available as .getCause on the exception.

The ‘Component’ library makes no attempt to recover from errors in a component, but you can use the :system attached to the exception to clean up any partially-constructed state.

Since component maps may be large, with a lot of repetition, you probably don’t want to log or print this exception as-is. The ex-without-components helper function will remove the larger objects from an exception.

The ex-component? helper function tells you if an exception was originated or wrapped by ‘Component’.

Idempotence

You may find it useful to define your start and stop methods to be idempotent, i.e., to have effect only if the component is not already started or stopped.

Other kinds of components

Any type of object, not just maps and records, can be a component if it has no lifecycle and no dependencies. For example, you could put a bare Atom or core.async Channel in the system map where other components can depend on it.

Test doubles

Different implementations of a component (for example, a stub version for testing) can be injected into a system with assoc before calling start.

Notes for Library Authors

‘Component’ is intended as a tool for applications, not resuable libraries. I would not expect a general-purpose library to impose any particular framework on the applications which use it.

That said, library authors can make it trivially easy for applications to use their libraries in combination with the ‘Component’ pattern by following these guidelines:

Never create global mutable state (for example, an Atom or Ref stored in a def).

Never rely on dynamic binding to convey state (for example, the “current” database connection) unless that state is necessarily confined to a single thread.

Never perform side effects at the top level of a source file.

Encapsulate all the runtime state needed by the library in a single data structure.

Provide functions to construct and destroy that data structure.

Take the encapsulated runtime state as an argument to any library functions which depend on it.

Customization

A system map is just a record that implements the Lifecycle protocol via two public functions, start-system and stop-system. These two functions are just special cases of two other functions, update-system and update-system-reverse. (Added in 0.2.0)

You could, for example, define your own lifecycle functions as new protocols. You don’t even have to use protocols and records; multimethods and ordinary maps would work as well.

Both update-system and update-system-reverse take a function as an argument and call it on each component in the system. Along the way, they assoc in the updated dependencies of each component.

The update-system function iterates over the components in dependency order: a component will be called after its dependencies. The update-system-reverse function goes in reverse dependency order: a component will be called before its dependencies.

Calling update-system with the identity function is equivalent to doing just the dependency injection part of ‘Component’ without Lifecycle.

Copyright and License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.