Liking cljdoc? Tell your friends :D

Pedestal with Component (Redux)

As your Component-based Pedestal application grows, you might see some growing pains. The theory and practice of Component is that each component gets exactly the dependencies it specifically needs …​ no more.

And yet, we have this :components component that, in a fully-featured application, can be expected to grow into a big grab-bag of dependencies. This is a "Big Ball of Mud" - a design antipattern that Component was created to avoid.

Pedestal has optional capabilities to define interceptors and handlers as components, each with individual dependencies. By embracing this approach, we end up with a more focused system map:

digraph G {
 ":pedestal" -> ":route-source";
 ":route-source" -> ":handler/get-greeting" [label=":get-greeting"];
 ":handler/get-greeting" -> ":greeter";
}

Again, with this tiny toy of an application, the structure is quite flat.

Before we get started on how this is implemented, there are some tradeoffs to consider:

  • Greater complexity: more components and more dependencies

  • Reloading is for functions not objects

  • Debugging into methods is harder [1]

To expand on that reloading issue: when you reload namespace changes, everything does get replaced. For functions, the fully qualified function name will be shared between the old code and the new.

For Clojure records, after reloading namespaces, the old record instance (in the system map) will still be in place, with the old JVM class for that instance. So, changing the implementation of a method of a record will not see any effect, at least until the system map is rebuilt.

So, keep those concerns in mind while we describe the revised application. This time, we can build from the bottom up, starting with the greeter component.

:greeter component

src/app/components/greeter.clj
link:example$component2/src/app/components/greeter.clj[role=include]

This namespace is unchanged from the prior guide.

handlers namespace

A new namespace, for components that are interceptors or handlers, has been created.

src/app/components/handlers.clj
link:example$component2/src/app/components/handlers.clj[role=include]
1api:definterceptor[] extends the behavior of clj:defrecord[].
2As with a record, we start with fields of the record.
3The handle method is allowed and definterceptor automatically adds the correspondng protocol.
4The implementation can directly reference the greeter field.
5It’s always good form to provide a function to create a new component instance.

The definterceptor macro streamlines the process of creating a record type for a component; it automatically adds the protocol (api:Handler[], api:OnEnter[], api:OnLeave[], or api:OnError[]).

It also quietly adds an implementation of api:IntoInterceptor[ns=io.pedestal.interceptor].

routes namespace

Next up the component heirarchy is the component used to generate the application’s routes. It depends on the handlers and other interceptor components that are referenced in the routes.

src/app/routes.clj
link:example$component2/src/app/routes.clj[role=include]
1Amazingly, no dependencies are needed for this namespace.
2The routes function is provided with the RoutesSource component and can extract the get-greeting dependency.
3The :route-name option is now omitted, and the name of the interceptor is used as the route name.

pedestal namespace

src/app/pedestal.clj
link:example$component2/src/app/pedestal.clj[role=include]
1The component has a dependency on route-source, and manages the connector field.
2This is where the RouteSource component is used.

system namespace

This all comes together in the app.system namespace where all components and dependencies get declared:

src/app/system.clj
link:example$component2/src/app/system.clj[role=include]
1A map can be used when the local field does not match the system map key.

Other code

All the other namespaces (primariy, the tests) are unchanged between the two versions of the application.

The Path So Far

In the guide we extended the prior application to fully leverage the capabilities of the Component library. We demonstrated how the definterceptor macro streamlines creating components that acts as both components and interceptors (or handlers), and we saw how each component had only explicit dependencies on exactly what other components it directly interacts with.


1. This is my personal experience using Cursive and IntelliJ. Often, it is not possible to set breakpoints inside methods.

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close