Living the Lean Startup with clojure.spec

I remember when I first read the The Lean Startup. Afterwards, it seemed so obvious, (as is common with very good ideas). It was all about applying the scientific method to startups, coming up with hypotheses about what you think your product should be. Then building tests to validate them and, in the process, avoiding wasted time, effort, and resources. In software terms:

Build the minimum functioning features that can get the needed feedback.

One of the most interesting things that I've seen over the last year is the Lean Startup methodology enhanced by the use of clojure.spec. During an engagement with Reify Health, an innovative healthcare startup, it was used to great effect to get faster feedback and to build a better product.

Building Lean

A common modern web application is the single-page application (SPA) paired with a server API. Building out the entire API and front end application is a good chunk of work and requires not only a good understanding of the domain but also knowing what data is going to be important. To really know that takes feedback from users and requires putting a working system in their hands. From a Lean point of view, this should be done is the smartest way possible with the least waste.

This is where one of the unique aspects of clojure.spec comes in. By creating specs for our data using custom generators, we can make the fake data come to life. For example, consider the spec for a name. We can define a list of sample names:

(def sample-names #{"Arlena" "Ilona" "Randi" "Doreatha" "Shayne" ...})

Then create a spec for the name with a custom generator using it.

(s/def ::name (s/with-gen
                       (and string? not-empty)
                       #(s/gen sample-names)))

Finally, we can generate sample data for it. In this case, we are just getting one but we could ask for as many as we want.

(ffirst (s/exercise ::name))
;=> "Ilona"

These specs can be composed together in nested data structures that describe the application. Best of all, using the generators, the data is produced in a random but consistent way. It allows us to create a fake local database that can back the SPA and have it function in a realistic way. More importantly, the product owners can get feedback on the application without having to build out the back end part of it yet.

Building an application this way is smart. The front end features can evolve rapidly in response to actual user feedback and not incur any unneeded effort on the server side. Eventually, when the feature/hypothesis is proved out, the back end can be constructed and the specs can be leveraged even more with sharing.

Sharing specs across the front and back end

One of the killer features of Clojure is that you can share code between Clojure and ClojureScript with .cljc files. This means you can also share specs. Sharing specs in this way allows for the validation of the data flowing across the boundaries of system. For example, the data for a customer endpoint can be validated before it goes across the wire on the server side and again once it hits the front end application. Specs can be validated with the s/valid? function:

(s/valid? spec value)

If a spec fails this validation, an error can be integrated with the full explanation of what was wrong with the s/explain-data function:

(s/explain-data spec value)

For an quick example, let's take the case of an endpoint returning a list of customers. Each customer is a map of data consisting of a customer-id, name, and state. Using our ::name from above, we'll create a couple more specs for the customer id and state. Finally, we'll create a spec for the ::customer map and the collection of ::customers.

(s/def ::id int?)
(defn state? [s]
   (-> (filter #(Character/isUpperCase %) s)
         count
         (= 2)))
(s/def ::state (s/and string? state?))
(s/def ::customer (s/keys :req-un [::id ::name ::state]))
(s/def ::customers (s/coll-of ::customer))

At this point, it would be useful to have a function to validate a value against a spec and if it isn't valid, throw an error with the full spec problem.

(defn validate
  [spec value message]
    (when-not (s/valid? spec value)
      (throw (ex-info message (s/explain-data spec value)))))

Now we can validate a list of customers on the api-server to make sure that they are valid before sending to the client.

(validate ::customers [{:id 1 :name "Susan" :state "OH"} {:id 2 :name "Brian" :state "CA"}] "Bad customers")
=> nil
(validate ::customers [{:id 1 :name "Susan" :state "OH"} {:id 2 :name "Brian"}] "Bad customers")
;=> 
clojure.lang.ExceptionInfo: Bad customers
clojure.spec/problems: ({:path [],
:pred (contains? % :state),
:val {:id 2, :name "Brian"},
:via
[:mynamespace/customers
:mynamespace/customer],
:in [1]})

Likewise, the client can validate the customers when it receives it with the same spec before it does any processing on it. Let's take a step back and look at what this lean, spec driven development process gives us.

Summary

  • Using clojure.spec to generate realistic looking data allows product owners to get fast feedback from users without incurring the development expense of building out the back end portion on unproved schema assumptions.
  • Specs can be shared across the front and back end in .cljc files, enabling code reuse and eliminating more wasted effort.
  • Use of spec to validate data across system boundaries at the server and client ensure application stability and integrity, building a quality product.

After seeing clojure.spec used this way to build an application, it seems obvious, (again like most good ideas). It's a powerful tool for startups, or any business, to build applications in a smart and lean way.