Popup Cards and Clojure

Cody Canning
Lovepop Platforms
Published in
5 min readJun 6, 2018

At Lovepop we use Clojure to write server-side web services in the e-commerce and design spaces. We have a few Clojure applications that power our 3D pop-up card customization platform. This platform builds and renders custom 3D greeting cards and wedding invitations from a library of design assets created and uploaded by our highly-skilled designers.

We chose Clojure to meet this challenge for several reasons:

Speed of development

Clojure is designed for rapid development. It features a lightweight, concise syntax. It’s dynamically typed and provides a REPL, allowing for fast experimentation and iteration. Its core collections are persistent data structures (immutable) and it embraces tenets of functional programming like referential transparency (pure functions) and modularity (namespaces) allowing for air-tight reasoning. State is isolated and managed via dead-simple concurrency primitives. And when all else fails, macros enable language-level retooling and abstraction.

Maturity and Reach

Our system embraces several key technologies:

  • SVG for composable 2D graphics
  • Formats like .OBJ for 3D geometric models
  • Open-source 3D renderers for generating photorealistic pop-up designs

And so we knew that it would be wise to leverage a mature platform like the JVM with access to a vast ecosystem of applicable, battle-tested libraries. Moreover, the JVM is a ubiquitous, fast, highly-optimized machine. Frameworks old and new are almost guaranteed to provide Java bindings. Clojure compiles to Java bytecode and provides interop for directly integrating those Java libraries and frameworks. Finally, if we need our reach to extend client-side there is ClojureScript which compiles to JavaScript and is a major driver of innovation in the UI space.

Design Philosophy

The price of reliability is the pursuit of the utmost simplicity.

— Tony Hoare

The Clojure language and community fully embrace simplicity as a North Star design goal. When it comes to stability, clojure.core takes an accretion over breaking changes approach. This ethos is a great foundation for building reliable, extensible applications. Lastly, Clojure was designed with an eye for performance and scale — there’s no worry that we’re going to have to migrate to another language once we discover a performance bottleneck 6 months in.

Our Stack

Clojure web applications are composed of libraries. The community eschews the traditional concept of a heavy web framework and tends toward smaller modules that can be swapped in and out. Our applications were built along those same lines with the Twelve-Factor methodology in mind. While we’re always growing and cultivating our applications, here’s a snapshot of the open-source tech and practices we built on.

Configuration

There are quite a few configuration libraries in Clojure. We initially used Environ as it embraces the 12 Factor App pattern and is very simple. Over time we felt the need for something a bit more featured and turned to cprop. Our workflow is to manage configuration for a project via a single config.edn which can be overridden during development via a local dev.edn. Environment variables retain highest priority and map back to explicit keys in the config. Cprop parses environment variable values into Extensible Data Notation so we have the convenience of typed values without having
to write custom string parsers or define type coercions.

HTTP and API Layer

Ring is the core abstraction of the HTTP specification, modeling requests and responses as data and providing both middleware and adapters. Compojure handles routing on top of Ring with macros for HTTP methods like GET and POST nested in route contexts with handlers that take Ring-compatible request data and produce Ring-compatible response data.

We use Compojure API to add interactive documentation (via Swagger), content negotiation, and request/response validation via Schema (see Runtime Typing below). For an HTTP client we use the community standard clj-http which also implements the Ring protocol for requests and responses. Notably, it provides request/response-body coercion (as byte-array, as string, etc.), asynchronous requests, and connection pooling. We have a light wrapper function around clj-http.client/request* which adds request retries with configurable strategies as well as logging and validation.

Runtime Typing

While Clojure’s dynamic type system is flexible and great for prototyping it’s often desirable to capture specific characteristics of the data you’re working with. We obviously want the code we write now to be readable and extensible in the future. There are a few things we do on that front; one of them is to use Schema, a library for declarative data description and validation. Schema effectively provides an API which leverages functions and Clojure’s core collections to specify data, including function argument and return values. Here’s an example from a namespace dealing with transformations of scene descriptions for 3D renders.

Runtime validation of function signatures is configurable and à la carte. Our data schemas plug right into Compojure API to provide request and response validation and auto-generated Swagger docs as well. Clojure’s most recent release (1.9) includes an alpha version of the core team’s clojure.spec library, which is a similar, more fully-featured tool in this space.

Naming and Formatting Conventions

Another approach we take is to follow consistent naming and formatting conventions. We utilize code linters and static analyzers (cljfmt, bikeshed, eastwood, joker ) to enforce some of these rules, but it is also done through code review. Naming conventions in particular can be really helpful to connote information about types or developer intent in a dynamically-typed language. Inspiration for these conventions was drawn mainly from resources like Readable Clojure and Encore, as well as from Lisp tradition:

foo!      =>  a function with emphasized side effects
foo? => a predicate (returns truthy or falsey)
foo!? => a predicate with emphasized side effects
foo$ => an expensive function like a network call or DB query
*foo => a dereference-able value (atom, delay, etc.)
_ => unused value (often during destructuring)
?foo => emphasize this value could be nil (also maybe-foo)
foo* => a variation of foo
*foo* => dynamically bound var ("earmuffs")
+foo+ => compiler-inlined constant like pi
->foo => coerce or convert to foo (also as-foo or to-foo)

Not everyone is going to agree on these conventions. Naming things is hard. At the very least this has become a useful exercise to get developers thinking about what information their names should convey and how to convey it.

Application State

Mount manages application state. The defstate macro creates a state and specifies its behavior on app reloads:

In this example we create an async channel on app start and close that channel on app stop. We no longer have to worry as much about remembering to close this channel, and our management of its state is isolated, not scattered across the application. Crucially, during development we can reload application states (or a subset of them) whenever we need to via (mount/start) and (mount/stop). Lastly, states are top-level names and can be required by other namespaces or in the REPL as per Vars. Without a tool like mount we’d have to write ad-hoc solutions for our state lifecycles and hand-roll reloadability. Similar libraries include Component and Integrant.

Takeaway

Our team at Lovepop has been using Clojure for two years now. Overall, our experience has been very positive: our tools have enabled us to be succinct and productive. As a result we’ve been able to maintain laser-focus on our product: customizable 3D popup cards. I can’t stress enough how working at an appropriate level of abstraction in a language that optimizes for uncomplicated reasoning is both an output and a creativity multiplier. The most vital engineering we can do is that which ensures moments of surprise and wonder for anyone who ever opens a Lovepop.

--

--