OVO Tech Blog
OVO Tech Blog

Our journey navigating the technosphere

Ivan Kerin
Author

Share


Tags


Contract Driven Development

Let's look at a simple story from the life of a dev team.

Bob and Alice are great developers. Alice can write amazing api backends and she's great with databases and servers, while Bob is all about the frontend, and likes to joke that he's allergic to backend stuff.

Bob and Alice receive a task - they are told to implement "feature flags". They come together and figure out a plan - lets just have a "feature" endpoint and go from there:

$ curl https://api.example.com/v1/feature/my-new-feature
> { active: true }

Alice puts on her headphones, disables her notifications and starts cranking out this feature. Bob could start working on the frontend bit, but since he doesn't want to bother Alice too much, he lets her finish that endpoint so it's easier to actually build the frontend with a working API. He'd better work on that landing page animation in the meantime.

Next day Alice is done and deploys it to a staging environment. Great. Bob takes the whole day to finish that animation though, since he wants to make it extra smooth. No worries, he'll start tomorrow.

Ok Bob is now ready to go for it. When he starts working though, he realises they need to change the api. What an unexpected development!

You see the feature flag must work on a user based principle, not just globally. Alice has started on that other bug yesterday though and still hasn't finished. And she has told Bob a million times now that switching contexts is bad for everyone's health. So now Bob either needs to wait, be a douchebag or start working on something else.

There has to be a better way!™

Since the title of this is "Contract Driven Development" you might have an inkling where things are headed. If we had a standard, verifiable way to express our API, both Bob and Alice can hash out a prototype definition and start working on it from both ends simultaneously. When they discover they need changes, they can communicate and resolve it, but since both of them are already working on the same task it becomes a lot simpler to fix any of their initial assumptions.


This is also a lot more scalable. We can replace Bob and Alice with whole teams. Coordinating changes to apis for microservices becomes a lot more straightforward.

Good contracts for apis are tools like GraphQL and OpenAPI, that have a lot of nice tooling around them to build mock apis, write tests, validates inputs / outputs and generate Types.

Laminar

Since we couldn't work with GraphQL for a variety of reasons, we decided to go all in with OpenAPI. At the business value team from OVO we introduced Laminar a tool that would generate the TypeScript types for both client and REST endpoints based on the OpenAPI spec.

In the example above, Alice and Bob would go and agree on the OpenApi Spec like so:

openapi: '3.0.0'
info:
  title: 'Feature Flags'
  version: 1.0.0
paths:
  /v1/feature/{id}:
    get:
      parameters:
        - name: 'id'
          in: 'path'
          required: true
          schema:
            type: 'string'
      responses:
        200:
          description: 'User'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeatureResponse'
components:
  schemas:
    FeatureResponse:
      type: object
      properties:
        active:
          type: boolean

And the tooling would use the schema to validate the data for the endpoint. It will check client's request, as well as the server's response. That allows us to also have TypeScript Types for client (using axios for the client) and server.  

yarn laminar api --file feature-flags.yaml --output __generated__/feature-flags.yaml.ts

yarn laminar axios --file feature-flags.yaml --output __generated__/feature-flags.yaml.ts

We make those commands part of our build process, so we don't include the __generated__ files in version control. And since TypeScript will check its types at compile time, we would not be able to even compile an incompatible client or server with a given OpenAPI schema.

A contract is worth a thousand words

Another approach to build something like this would be generating the OpenApi schema from the code itself. There are tools that allow you to annotate your source code with OpenAPI or GraphQL which accomplishes much the same task of making sure your request and response are always correct. Code is very easy to change however, especially if your endpoint relies on some internal types of your system (models / entities).

If for example, your models have a "decimal" field, and the external library you depend on changes how it serializes itself after an update, you can inadvertently change your openapi schema, without even realizing it.

This is not that big of a problem if your team is writing both the client and the server, as fixes can be quickly rolled out, and you generally take care of the changes in unison. But if you have another team (or teams) relying on your data, that can lead to some heated exchanges with the affected teams.

If your code makes sure, at compile time, that what you're sending and receiving is the exact same thing, then any such change to the api would need to be made knowingly and explicitly to the OpenAPI schema.

A final note - OpenAPI can get unwieldy after a while if it gets too big. But there is a lot of tooling to help you along. First of all, you can easily split it into several files, you can also use things like SpotLight to design / change your openapi schema in a more human readable way.

View Comments