Evolving Rails: Retaining Backward Compatibility

When evolving a codebase, there are two kinds of changes. The first is an innocuous internal change that nobody else is relying on – aka a true refactoring as per Martin Fowler’s canonical definition. The second is a public-facing change that will impact many others.

When I say “others,” I am referring both to other parts of your own codebase as well as other users entirely. In a web app, your public API is usually the set of URLs that users can use in order to interact with your application. It might also include a client library that you distribute in order to facilitate the use of your API.

In a library or plugin, your public API is the set of methods that applications use to make use of the functionality you expose. In the case of Rails, our public API is made up of methods like before_filter, render, and when_fresh.

Version and Document Your Changes

As with Rails itself, you want to make changes to your public APIs in explicit versions. In the case of APIs that are made up of publicly available URLs, this means versioning your API URLs. (John Barnette has a very good write-up on this.)

If you’re releasing a library, be it a gem or a Rails plugin, release a new minor version (1.2) of your library if it contains changes to the public API. Release a new major version (2.0) of your library if it breaks existing APIs.

If You Must Break Compatibility, Deprecate First

Ruby is an extremely flexible language. If you must break backward compatibility, first release a minor version that supports both APIs, marking the old one as deprecated, then remove the deprecated API in a future version.

This is something Rails has done reasonably well for a number of years. For example, Rails deprecated support for templates with .rhtml extensions several releases ago, replacing them with templates ending with .html.erb. Both options were supported, but Rails 3 (a major release) will finally remove support for the old API.

In terms of URL APIs, you can achieve this by keeping around support for older versions of the API, but phasing out versions as you move forward. For instance, it might be ok to drop support for version 1 of such an API when version 3 is released.

Make sure the users of your code receive deprecation warnings when they use the older API, so they are aware that they are using code with a limited shelf-life.

Set Expectations Correctly

If you have a public API, make sure your users know the approximate schedule for expected changes. If you plan to release new versions of the API every few months, and remove support for versions that are more than two versions old, communicate that with your users. That way, they’ll know when to build in time to keep their code humming along.

In the case of a library, make sure users know what to expect when they upgrade to a new version. Rails typically does not break backward compatibility with the public-facing API in minor versions, although it does add deprecation warnings. Rails 1.0, 2.0 and the upcoming 3.0 made and will make larger changes, mostly by removing support for APIs that were deprecated in previous releases.

Make Use of Ruby’s Flexibility

Ruby is a very powerful and flexible language, and allows you to easily support legacy APIs in an isolated way. For Rails 3.0, we were pretty sure we would need to break a non-trivial amount of backwards compatibility (after all, ActionController on Rails edge is mostly a new codebase). It turned out, however, that we were able to maintain backwards compatibility for all but the most obscure cases.

For instance, we have removed support for render :layout => nil, which was a synonym for render :layout => true in Rails 2.x, but the opposite of render :layout => false.

However, we have maintained support for things like removing the leading slash in render :template => "/foo" and making render :layout => "layouts/foo" the equivalent of render :layout => "foo".

In order to keep the changes isolated, we created a new Compatibility module which is included in ActionController::Base by default but can be opted-out-of to remove features that we plan to remove. An example:

def _find_layout(name, details)
  details[:prefix] = nil if name =~ /blayouts/
  super
end

In this case, the Compatibility module overrides the default _find_layout method, and handles the case where the user included layouts in the layout they specified. We then call super, which runs the original behavior after we’ve normalized the data.

The key takeaway is that one way of handling backwards compatibility is to use Ruby’s flexibility to normalize the old functionality into the new functionality in an isolated place. In the case of Rails, we know that we can remove the Compatibility module wholesale when we next do a major version bump.

For HTTP APIs, use External, not Internal Redirects

It can be tempting to simply use Rails’ router to point URLs in transition to their new location. However, this will not inform clients that the URL they are using has changed. If you use a 301 status code (Moved Permanently), browsers and other clients have an opportunity to learn about the new location and behave appropriately.

Think about a 301 status as the equivalent of a deprecation warning for HTTP APIs, allowing clients to take corrective action before you take the final step of removing support for the URL entirely.

If you provide a client library for your API (as you should), print a deprecation notice when your server returns a 301 redirect. This gives you the ability to warn your users that they will need to update their client by making a change to the server only.