Clever Engineering Blog — Always a Student

Using Go’s interfaces at Clever – more than just easy collaboration

By Colin Schimmelfing on

A few months ago Clever had the opportunity to give a talk to the GoSF Meetup group (the “largest Go meetup group in the world”!). Mohit and Alex discussed their experience creating Sphinx (our rate limiting service) and the usefulness of Go’s interfaces in doing so.

Here are the slides:

There are a few reasons that we found Go’s interfaces to be so useful in developing Sphinx.

Interfaces are useful for:

  • Collaboration between busy engineers
  • Simple, non-repetitive testing
  • Swappable behavior, allowing flexibility without compromising safety

Collaboration:

Sphinx had some intricate parts interacting together, but these parts were not always being worked on by the same person. While interfaces in all languages promise this flexibility, in particular Go interfaces are simple to use and allow us to very quickly mock out objects that haven’t yet been implemented.

Simple, Non-repetitive Testing:

Using the power of interfaces in Go for testing is something you might think less about. If you write tests against an interface, you’ve created a gauntlet that any new implementation must run before being accepted. This means that a bug found in one implementation can be protected against in others once the test case is written, and any new implementation does not require tons of test porting work (one of the least interesting kinds of work!). Additionally, it keeps your code concise, as you can see by the third slide.

Example interface:

type Storage interface {
   Create(name string, capacity uint, rate time.Duration) (Bucket, error)
}

Example test that should work regardless of implementation:

func CreateTest(s Storage) func(*testing.T) {
    return func(t *testing.T) {
 	now := time.Now()
	bucket, err := s.Create("testbucket", 100, time.Minute)
	if err != nil {
	    t.Fatal(err)
	}
	if capacity := bucket.Capacity(); capacity != 100 {
	    t.Fatalf("expected capacity of %d, got %d", 100, capacity)
	}
	e := float64(1 * time.Second) // margin of error
	if error := float64(bucket.Reset().Sub(now.Add(time.Minute))); math.Abs(error) > e {
	    t.Fatalf("expected reset time close to %s, got %s", now.Add(time.Minute), bucket.Reset())
	}
    }
}

Example test for different implementations:

// Tests for the Memory Implementation
func TestCreate(t *testing.T) {
    CreateTest(New())(t)
}

// Tests for the Redis Implementation
func TestCreate(t *testing.T) {
    s, err := New("tcp", os.Getenv("REDIS_URL"))
    assert.Nil(t, err)
    CreateTest(s)(t)
}

Swappable behavior, allowing flexibilty without compromising safety:

The power of this flexibility was a bit of a surprise, and helped us avoid extensive, boring testing on staging. Since Sphinx is a rate-limiting service, it’s a fairly dangerous and hard-to-test piece of code. Test cases might reflect some regular usage patterns, but we get millions of requests per day and wildly different traffic patterns around the clock – we can’t find all the edge cases.

While creating a test environment and replaying a week’s worth of traffic is possible, it’s expensive and not that interesting. We found a better way – using interfaces, we rolled out a “risk-free” version of the code that logged a message each time that we would have rate-limited a customer. This allowed us plenty of time to track down misbehaving customers and help them adjust their query patterns – a much friendlier way of handling the situation than sending a 429 code and hoping that they’ve implemented exponential backoff and retry correctly.

This was as simple as:

type Handler interface {
    ServerHTTP(ResponseWriter, *Request)
}

func NewHTTPLimiter(rateLimiter ratelimiter.RateLimiter, proxy http.Handler) http.Handler {
    return &httpRateLimiter{rateLimiter: rateLimiter, proxy: proxy}
}

func NewHTTPLogger(rateLimiter ratelimiter.RateLimiter, proxy http.Handler) http.Handler {
    return &httpRateLogger{rateLimiter: rateLimiter, proxy: proxy}
}

Since the two versions of the code use the same interface, we were very confident that no surprises would greet the “risky” code rollout!

Conclusion:

If you’d like to hear it from Mohit and Alex, our engineers who wrote Sphinx, check out the talk:

If you want to use Go daily while bringing the promise of technology to the classroom, come join our team!

If you’d like to discuss, please check out our post on Hacker News.