Think Big

Stencil Challenges - Versioning

SOA After the Engineering organization at Bigcommerce embraced service-oriented architecture (SOA), it made the decision easy for us to start new projects with independent parts. Since old habits are difficult to break, we started our Theme Engine project, Stencil, by placing all our code in a single repository. We quickly realized we were traveling down the same road of a monolithic code base, so we began splitting the project into smaller components so it would be easier for us to digest. We didn’t want a single entity to do “too much”, and this help simplify the responsibility of each service. Towards the end of the project, we ended up with four services.

monolithic

Everything was dandy inside our development environment -- all the codebases were synchronized with all the moving parts. As we began preparation for shipping our product, we quickly realized a big problem: What happens when we push the different parts to production? How do we safely upgrade one piece without affecting the other?

Our team came together to brainstorm a solution to the problem. After coming up with a few different options, one solution emerged from the rest: Versioning.

brainstorm

We took the same approach as any other public API by applying a versioning mechanism to our internal APIs which gets communicated between the services. Each service sends a "version" tag as part of its payload and we use the tag to help the services communicate with each other. When two parties negotiate for a request, they would read each other's "version" tag and from that determine the appropriate code path. If the version is a mismatch or no longer supported, an error will be thrown. Let's walk through two scenarios, one with versioning in mind and the other without. Here's how an API endpoint is typically implemented without versioning:

// service requesting user data
data = request('/user/1')

// user endpoint api service
return {'user_name': 'steve', 'balance': '25'}

After releasing this API to the public, you receive a request from a developer to add currency information along with the balance. With versioning, you can easily make the improvement and won't affect existing developers who haven't switched to the new API yet. Here's what the improved version looks like:

// service requesting user data
data = request('/user/1', {
   'api_version': '1' // specify api version
});

// user endpoint api service
if (request.api_version == 1) {
   // Started project in USD only
   return {'user_name': 'steve', 'balance': '25'}
} else if (request.api_version == 2) {
   // An improved API version after recognizing that "balance" needs a currency
   return {'user_name': 'steve', 'balance': '25', 'currency': 'USD'}
} else {
   return {'error': 'version not recognized'}
}

Furthermore, we took a simpler approach than the traditional api versioning. Instead of using major, minor and bug fix numbers, we decided to split versioning into two categories. The first category is for simple upgrades between the two services that will not affect any public APIs. For example, we would change the service we’re upgrading from version 1 to 2 and we would update the code for all dependent services to utilize the new version. The second category is used for long term, where little to no change will be made to the API. In this case, we used the date as the version number so it can help us identify the time we implemented the API and we can also use it in our logical comparisons by converting it to unixstamp. For example, you can do an if statement by comparing the dates:

date = unixtimestamp(data.version)
if (date <= unixtimestamp(‘2015_11_25’)) {
    // code specific to this version
} else { 
    // normal code path
}

Not only is versioning useful to keep two services in sync, it helps the me think and write code differently. I would spend more time thinking about how other people can consume their code and make conscious decisions when changing the code. As a consequence of deeper thinking, the quality of my code improved. After utilizing versioning in our theme engine, we began applying the same pattern to other projects. We would start organizing our files into separate folders by their version number to help keep track of the different versions:

code/
    |__v1/
    |   |__user.js
    |__v2/
        |__user.js

There are a few unsolved problems with versioning. The first problem is the amount of files you have keep in your project as your project grows. If you have multiple versions, it gets horrendously massive. The second problem is having if statements everywhere in the code to handle the different code versions. This can get messy and tricky. Both problems will also lead to tech debt as time progresses. The solution we came up to mitigate these problems is having a migration plan to move developers to use a newer version. We would send out an email to developers communicating a target date in the distant future (approximately around 6 months) when we will stop supporting a certain version. With this approach, we can help and support them migrate over to the newer version slowly.

Here at Bigcommerce, we strive on creating the best product available. We do this by providing developers with the lessons we've learned over the past year. I learned a lot with versioning when writing services code. Versioning really helps if you start early with versioning because it will save time in the long run without have to rewrite a lot of code to support a new feature. So the next time you’re writing a service, you should plan on versioning your codebase early so it will be compatible for upgrades or migration.

Hau Nguyen

Frontend Engineer at Bigcommerce

place San Francisco, CA, USA create