Common Facade APIs

This is a draft, but I believe it’s worth sharing in this form.

One of Jujus great triumphs is the backwards compatibility of Facade versioning. Although initially a steep learning curve, overtime makes sense, except when it doesn’t! I believe at the heart of the great Facade versioning is a problem that hasn’t manifested itself in its fullest, but can be worked (read as hacked) around, which may lead to believing it is benign. It’s an anti-pattern of common facade APIs and the methods they bring along.

Examples are always best for illustrative purposes:

  1. anything in apiserver/common - mainly looking at our old friend networkingcommon

These common API types give your facade super powers if you embed them. What’s not to love, they add the ability to make a Facade suddenly gain the ability to reason about Life with one line of code (see below).

type FooFacade struct {
     *common.LifeGetter
}

The problem occurs when you want to make changes to LifeGetter in particular Life (in our contrived example), but they’re breaking changes? How do you do this sensibly and in a pure way? The answer I believe is that you can’t, when trying to keep the codebase pure!
You’ve lost the ability to do the logical thought through, tried and tested way of bumping the facade and overriding the previous method. You now have to resort to tricks and hacks to give your facade the ability to talk Life v1 and Life v2.


We should stop embedding the types into Facades. We shouldn’t allow common types to be dictating our Facade APIs. It’s not the responsibility of common code to do that. Instead we should create common functionality that the facades can employ to do the heavy lifting. Even at the expense of re-usability of said commonality!

The change to fix this isn’t that dramatic, but laborious. It gives us a lot, without asking for much!

Changing the above example to the following example should be clear:

type FooFacade struct {
    lifeGetter *common.LifeGetter
}

func (f *FooFacade) Life(args params.Entities) (params.LifeResults, error) {
   return f.lifeGetter.Life(args)
}

You may ask, what does this give us, but this gives us a lot, we can now version lifeGetter easily and simply.


You can take this one stage further and give us even greater abilities by then not depending on a concrete implementation.

type LifeGetter interface{
   Life(params.Entities) (params.LifeResults, error)
}

type FooFacade struct {
    lifeGetter LifeGetter
}

type FooFacadeV1 struct {
    FooFacadeV2
}

type FooFacadeV2 struct {
    FooFacade
}

func (f *FooFacadeV1) Life(args params.Entities) (params.LifeResults, error) {
   return f.lifeGetter.Life(args)
}

func (f *FooFacadeV2) Life(args params.Entities) (params.LifeResults, error) {
   return f.lifeGetter.Life(args)
}

Each Facade version can then have their own implementation of Life and only at instantiation time do you give each Facade the correct version of lifeGetter. We’ve gained the ability of easily versioning a system and we’ve now enabled better testing using Liskov substitution and Dependency inversion principles.

The facade is testable at the unit level and as an integration. You can get 100% coverage of the facade and we can ensure that it’s the correct logic.


If you squint slightly the LifeGetter interface is a collection (repository) of commands and that’s a well trodden pattern. I’ve and @manadart used this as a great example for ReloadSpaces, one that I implore we use through out the code base.

1 Like

As a way to migrate to this, can’t you do this change when you need to bump a common api? (Things that embed it stop embedding it, and instead just thunk to it?)

That’s a very good method forward as well, so :+1: from me. Having said that, I would like one way to do it and stick to that if possible.

1 Like

I like the solution, especially as expanded. It introduces some apparent code duplication, but not of the sort that’s too much of a headache to manage. And what it buys is us worthwhile!

Can we please promote this to #devel rather than keeping it internal?