I ended the previous part section with a definition (in Go) of the core functionality of the features service.

There is almost certainly more functionality to be discovered, but to implement our simple requirements, I need this.

type FeaturesAPI interface {
	ResourceHasFeature(resource, id, feature string) (bool, error)
	AddFeatureToResource(resource, id, feature string) error
	RemoveFeatureFromResource(resource, id, feature string) error
}

To implement the ResourceHasFeature call, I need to be able to lookup a resource and and work out whether or not that resource has a feature.

In my diagram, I had a “Feature Store”, again, this is an interface.

Feature Store

package store

type FeatureStore interface {
	AddFeature(resource, id, feature string) error
	GetFeaturesForResource(resource, id string) ([]string, error)
	HasFeature(resource, id, feature string) (bool, error)
	RemoveFeature(resource, id, feature string) error
}

This is a fairly abstract store, this is Go, so pretty much anything can return an error to be handled, each store can presumably return its own error types, there might be value in standardising some of these errors, especially to improve HTTP response codes, being able to differentiate between a 404 and 500 can be especially useful when debugging client calls.

I believe that one of the hallmarks of good design, is that it’s language independent, as well as extensible.

Clojure

Clojure is very unlike Go, it’s a JVM-based functional language, an implementation of Lisp.

The same Feature Store in Clojure looks like this:

(defprotocol FeatureStore
  (add-feature [this resource id feature]
    "Records the association of the feature with the resource.")
  (has-feature [this resource id feature]
    "Returns true if the resource has the specified feature.")
  (remove-feature [this resource id feature]
    "Remove a feature from the resource.")
  (list-features [this resource id]
    "Remove a feature from the resource."))

In-Memory feature store implementation in Go

type InMemoryFeatureStore struct {
	features map[string]map[string]bool
}

func NewFeatureStore() *InMemoryFeatureStore {
	return &InMemoryFeatureStore{features: make(map[string]map[string]bool)}
}

func (iff *InMemoryFeatureStore) HasFeature(resource, id, feature string) (bool, error) {
	features := iff.features[resourceKey(resource, id)]
	return hasFeature(feature, features), nil
}

func (iff *InMemoryFeatureStore) AddFeature(resource, id, feature string) error {
	key := resourceKey(resource, id)
	_, ok := iff.features[key]
	if ok {
		iff.features[key][feature] = true
		return nil
	}
	iff.features[key] = map[string]bool{feature: true}
	return nil
}

This is the HasFeature implementation of a simple in-memory store, I’ve deliberately not made this thread-safe to keep it simple.

This code provides a NewFeatureStore package-function, which is the standard Go idiom for a constructor, and implements the HasFeature store method on InMemoryFeatureStore values.

This store is practically only useful for testing purposes, but it allows us to hook up a lot of code, without thinking about how I’ll actually store the features, this helps to check that I’ve identified an appropriate abstraction.

I can test this quite easily…

func TestHasFeatureWithNoFeature(t *testing.T) {
	store := NewFeatureStore()

	ok, err := store.HasFeature("pipeline", "8e674e29-a848-44a1-a26b-95aaadb96111", "review-apps-2")

	if err != nil {
		t.Fatal(err)
	}
	if ok {
		t.Fatalf("checking for feature failed: got %v, wanted %v", ok, false)
	}

}

func TestAddFeature(t *testing.T) {
	store := NewFeatureStore()

	err := store.AddFeature("pipeline", "8e674e29-a848-44a1-a26b-95aaadb96111", "review-apps-2")

	if err != nil {
		t.Fatal(err)
	}
}

func TestHasFeatureWhenResourceHasFeature(t *testing.T) {
	store := NewFeatureStore()

	err := store.AddFeature("pipeline", "8e674e29-a848-44a1-a26b-95aaadb96111", "review-apps-2")
	if err != nil {
		t.Fatal(err)
	}

	ok, err := store.HasFeature("pipeline", "8e674e29-a848-44a1-a26b-95aaadb96111", "review-apps-2")

	if err != nil {
		t.Fatal(err)
	}
	if !ok {
		t.Fatalf("checking for feature failed: got %v, wanted %v", ok, true)
	}
}

In-Memory feature store implementation in Clojure.

The Clojure implenentation is surprisingly similar.

(ns com.bigkevmcd.features.stores.in-memory
  (:require [com.stuartsierra.component :as component]
            [com.bigkevmcd.features.store :refer [FeatureStore]]))

(defn- resource-key [resource id] (str resource ":" id))

(defrecord InMemoryFeatureStore []
  component/Lifecycle
  (start [this]
    (assoc this :features-atom (atom {})))
  (stop [this]
    (dissoc this :features-atom))

  FeatureStore
  (has-feature [this resource id feature]
    (let [features @(:features-atom this)]
      (contains? (get features (resource-key resource id)) feature))))

(defn new-in-memory-feature-store []
  (->InMemoryFeatureStore))

This implements the Lifecycle protocol from component, and implements the FeatureStore protocol, and like the Go version, provides a short-cut constructor function.

Writing tests to exercise the code is again fairly simple…

(ns com.bigkevmcd.features.stores.in-memory-test
  (:require [clojure.test :refer :all]
            [com.stuartsierra.component :as component]
            [com.bigkevmcd.features.store :refer [add-feature has-feature remove-feature list-features]]
            [com.bigkevmcd.features.stores.in-memory :refer :all]))

(def feature "review-apps-1")
(def feature2 "review-apps-2")
(def resource-id "8e674e29-a848-44a1-a26b-95aaadb96111")
(def resource "pipeline")

(def feature-store (atom nil))

(defn with-feature-store [f]
  (swap! feature-store (fn [s] assoc s (component/start (new-in-memory-feature-store))))
  (f)
  (swap! feature-store (fn [s] (component/stop feature-store))))

(use-fixtures :each with-feature-store)

(deftest test-has-feature
  (testing "when a resource does not have a feature"
    (is (= false (has-feature @feature-store resource resource-id feature)))))

(deftest test-add-feature
  (testing "when a resource does have a feature"
    (add-feature @feature-store resource resource-id feature)
    (is (= true (has-feature @feature-store resource resource-id feature))))
  (testing "when a resource has multiple features"
    (add-feature @feature-store resource resource-id feature)
    (add-feature @feature-store resource resource-id feature2)
    (is (= true (has-feature @feature-store resource resource-id feature)))
    (is (= true (has-feature @feature-store resource resource-id feature2)))))

This creates a feature store as a fixture, and instantiates a new one per test, the test reads fairly close to the Go implementation.