Cohesive Microservices with GoKit

Whilst working on a cross-orchestration reference microservices application this week, my colleagues and I from Container-Solutions and WeaveWorks ported all of our simple Go microservices to the GoKit framework.

For simple microservicecs, Go is a great choice of language. It has low overheads and has a garbage collector. It’s fast and has a higher level of abstraction than C++. But there are complaints. Funny complaints ranging from an “un-googleable name” and “weird mascot” to quite reasonable ones like “no OOP” and “undebugable”.

Further to the language complaints, there are probably three elements slowing “enterprise” adoption (whether this is really a problem is for another time). 1) Operational cost - how much does it cost and continue to cost to develop and maintain an application (e.g. how easy is it to find developers?). 2) Tooling - are there enough tools to support enterprise development (e.g. CI/CD, IDE’s, etc). And 3) Framework support - the value/time ratio (e.g. how quickly can we develop a valuable application).

GoKit is a microserivces framework, with a set of idioms and a collection of libraries that aims to improve the design and flexibility of Go-based application. It doesn’t set out to be a platform; instead it tries to integrate into your platform of choice. I like this approach; it accepts the scope in which it lies. Application level, not orchestration.

Conversion from Go to GoKit

The (GoKit examples)[https://gokit.io/examples/stringsvc.html] are an excellent introduction to the architecture of a GoKit application. It promotes an onion model, which separates business concerns from middleware and transport layers. Following the workflow of the example, produces an application with approximately four classes (roughly equating to the different layers in the architecture model) and one large main class. There are approximately 350 lines of code. The number of lines of code in the equivalent “dumb” go application was approximately 50.

From the outset, my first and only concern was the increase in lines of code for little immediate value. However, my fears were quickly abated by the reduced coupling and increased cohesion. It became insanely easy to test the business logic.

As a concrete example, here is the code to create and perform a simple authorisation request:

// Auth service interface
type Service interface {
	Authorise(total float32) (Authorisation, error)
}
// Auth service response struct
type Authorisation struct {
	Authorised bool `json:"authorised"`
}
// Auth service contstructor
func NewAuthorisationService(declineOverAmount float32) Service {
	return &service{
		declineOverAmount: declineOverAmount,
	}
}
// Auth service instance
type service struct {
	declineOverAmount float32
}
// Auth service implementation
func (s *service) Authorise(amount float32) (Authorisation, error) {
	if amount == 0 {
		return Authorisation{}, ErrZeroPayment
	}
	if amount < 0 {
		return Authorisation{}, ErrNegativePayment
	}
	authorised := false
	if amount <= s.declineOverAmount {
		authorised = true
	}
	return Authorisation{
		Authorised: authorised,
	}, nil
}

It’s a little verbose. More than I would like, but probably no worse than Java. But note how there is no bloat from transport implementations (e.g. HTTP) or instrumentation (e.g. logging/metrics). These are housed elsewhere. We can now create a simple test to ensure that any payment request of zero produces an error:

func TestFailIfAmountIsZero(t *testing.T) {
	_, err := NewAuthorisationService(10).Authorise(0)
	_, ok := err.(error)
	if !ok {
		t.Errorf("Authorise returned unexpected result: got %v want %v",
			err, "Zero payment")
	}
}

Here, GoKit really shines. Because there’s no transport concerns, there’s no need to create or mock out any implementation. Of course, this decoupling can be achieved with other frameworks, (Spring, for example), but here it is effectively enforced by the architecture of the framework.

The Value of Free

GoKit also ships with a range of transport support and middlewares. It supports HTTP, Thrift and gRPC out of the box. But the real value is provided by the vast middleware support. Which includes rate limiting, logging, circuit breaking, load balancing and a whole lot more that I haven’t had chance to try. Add this to various service discovery schemes and monitoring system support, I’m happy to conclude that this easily produces the best Go microservice framework I’ve had the privilege of trying.

On one final note, I am lucky enough to have worked with the author of GoKit, Peter Bourgon. My concern about the excessive number of lines was abated by the promise of code generation capabilities in future versions of GoKit. He said he hopes to “reduce the amount of boilerplate (e.g. in service definition), by enabling the automatic generation of repetitive code”. I’m very much looking forward to this addition.