Liking cljdoc? Tell your friends :D

Server Architecture: Components and Beyond

This guide describes the architecture of Sweet Tooth API servers and the tools used to implement the architecture. It covers:

  • The components involved in handling a request

  • The relationships among the components

  • How Duct and Integrant compose and manage the components

  • How a request flows through the components

The term component is super generic! It’s siblings with module and sub-system in the "nebulous programming terms" family. For our purposes a component is "an instance of a computing thing that complies with an interface." An interface is a set of rules for sending and receiving messages, including both the transmission mechanism (direct function call, network call, etc) and the message structure.

For example, a machine is a computing thing with a TCP/IP interface. An HTTP server is a computing thing with an HTTP interface. Let’s look at these components more closely.

The "and Beyond" part of this guide’s title is bs, but it’s lovely bs. There’s a retail chain in America called Bed Bath and Beyond and the "and beyond" totally kills me. For some reason it sounds like they could make your wildest dreams come true, but no, the "beyond" part is actually useless Americana tchotchkes like Mickey Mouse mini waffle makers.

Related: I once found a blow dryer at Target whose packaging read something like: "Good for drying hair AND SO MUCH MORE" and I sincerely hope the copywriter enjoyed writing that. Thank you, noble soul, for reminding me that all of life is full of wondrous possibility. Even everyday consumer goods hold untold potential.

Sweet Tooth: good for writing single page apps AND SO MUCH MORE

API Server Component Decomposition

Handling an HTTP request requires the coordination of multiple components. This section breaks down high-level components into their subcomponents and describes their relationships.

Check out my this diagram of the components involved in handling an API request which looks like it was outsourced to a three year old:

top down handlers
Figure 1. wow a very well-drawn architectural diagram

The highest-level component is the machine (1). When an HTTP request is sent, the requestor’s only expectation is that some machine - virtual private server (VPS), Amazon EC2 instance, a lemon-powered raspberry pi - somewhere receives the request and sends a response. It doesn’t care about the machine’s OS or what processes are running.

The machine will be running a database (2). It will also be running an HTTP server (3). There’s nothing special about HTTP servers; they’re just some program someone wrote, only they happen to listen for HTTP requests and send HTTP responses.

For Clojure web apps, the HTTP server usually creates a database connection (4). The HTTP server also contains a request handler (5). In Clojure apps, the request handler is just a function which takes a request and returns a response.

the next couple paragraphs are a light overview of how a request gets transformed at every stage of its journey from machine to HTTP server to Clojure request handler. I’d love feedback on it: does it not provide enough detail? Does it belong elsewhere?

The relationship between machine, HTTP server, and handler, is represented by the boxes and lines at (6). The boxes represent an interface that communicate with the external world via some protocol. A protocol in this context is just some agreed-upon message structure that allows sender and receiver to understand each other. The machine interface is an open port, typically port 80 or port 443, and communication happens via TCP/IP.

The HTTP server is some process running on the machine assigned to listen to ports 80 and 443. It receives and sends messages that conform to the HTTP protocol. Internally, the HTTP server converts the raw text of an HTTP message into data structures that its supported programming languages understand. An Apache server has modules for converting a message to PHP or Perl. A Ruby server converts the message to some Ruby object.

Clojure is hosted on the JVM, and we use Java HTTP servers, most frequently a library called Jetty. Jetty converts a request to a Java object. In Clojure, we prefer to work with native Clojure maps and vectors, and developers most frequently use the ring library to adapt the request for Clojure. It converts the Java request object into a Clojure map with the keys :uri, :query-string, :request-method, :headers, and :body, plus a few more.

Ring allows you to define a Clojure function to handle requests - we saw that at (5). The request function takes a Ring request as an argument and should return a response, which is a map with the keys :status, :headers, and :body.

You can write a function to construct a Ring response any way you want to, but generally Ring request handlers are structured as a middleware stack (7) and a router (8). The Ring request map passes through the middleware stack, which transforms the request by adding, modifying, or removing the map’s keys. The ring request is then passed to the router, which routes the request, passing it on to an endpoint handler based on the request’s path (e.g. /topic/1) and method (:get, :post, etc).

Endpoint handlers typically perform CRUD (create, read, update, delete) operations on a database, and therefore they typically have a reference to the database connection (9).

Component Definition, Composition, and Initialization with Integrant and Duct

Now that we know what components are involved in building an API server and how those components are related to each others, let’s turn our attention toward the work we as developers have to do to implement this architecture. Implementing an architecture includes addressing how you define, compose, and initialize a system’s components.

To define a component is to establish its responsibilities and its interface. It also means choosing one or more language constructs to implement the notion of "component".

In object-oriented languages this process feels more solid somehow: components are defined by classes; the class’s public methods are the interface and the notion of "component" maps directly to classes. Things feel a bit more loosey-goosey in Clojure land — is a component a function? a namespace? a record? — but I’ll introduce you to techniques for defining components shortly.

Composing components: how do components reference each other? The two main approaches are to create a globally-accessible component that other components reference directly from anywhere, or to follow the dependency injection pattern. You’ll soon learn about how Sweet Tooth relies on the Integrant and Duct libraries, which implement dependency injection for Clojure apps.

Initializing components refers to the process of creating any objects or state the component needs, and calling a function or method to start the component if necessary. To initialize a request handler, you just create a function. To initialize a database connection pool you create an instance of a connection pool service, which might create some initial threads for db connections.

To get a Clojure API server running, you must first get a JVM process running. Within that process, you must initialize components in dependency order:

  • Initialize a database connection or connection pool

  • Initialize a request handler that references the database connection

  • Initialize an HTTP server with the request handler

What does it mean to "initialize an HTTP server" from within a JVM process? If you’re familiar with programs like Apache or nginx, you might be used to thinking of an HTTP server as a program that you launch from the command line, not as something that you start from within the process of a program you’re writing.

The thing is, anyone can just write a program that starts listening to a port. The tools are readily available. If you use your programming language’s standard libary to start listening for messages on a port and responding, congratulations: you’ve created a server!

Now if you care about things like performance and resilience, you’ll have to get a bit fancier. That’s why we have HTTP server libriaries. In the Java world, one of the most popular libraries is Jetty. It adds some structure to how HTTP requests are handled, and it takes care of managing resources like threads.

Initializing a Jetty server in your JVM process is basically a matter of creating an org.eclipse.jetty.server.Server object and calling its start method.

You could easily write something like this pseudocode to define, compose, and initialize your system’s components:

"start a server" pseudocode
(def db-connection (create-connection))
(defn handler [req] (update-db db-connection))
(defn start-server [] (run-jetty handler {:port 3000}))

(start-server)

I’ve seen plenty of Clojure API servers with code that looks like that, and that approach works fine.

As I’ve mentioned like a billion times now, Sweet Tooth uses Integrant and Duct to manage these architectural concerns. We’ll first look at Integrant, because it provides the foundation. Then we’ll look at Duct, a layer on top of Integrant that 1) makes it easier to create bundles of components to share and 2) makes it easy to configure components for different environments (dev, test, prod, etc).

So let’s look at Integrant so that you won’t have to listen to me say "In a minute we’re going to look at Integrant" anymore.

Can you improve this documentation?Edit on GitHub

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

× close