If you’ve ever had a deploy go bad because your new code broke someone else’s application, then this talk is for you. By making your changes backwards compatible, you can safely add features without worry, and deploy without downtime. We’ll discuss what backwards compatibility is, why it is both good and necessary, and how we can achieve it.
Ian Robertson
2. Agenda
Where does backwards compatibility matter,
and why?
Ways a ReST service can evolve.
Strategies for evolving a service in a backwards
compatible fashion.
Database backwards compatibility.
Best practices.
3. So you want to deploy a new version
The new version has a different API than the
old one.
Typically, web service
Also: java library
Also: schema design
4. Just upgrade the clients?
May not be able to upgrade all clients at the
same time.
Creates tightly coupled releases.
Cannot do partial rollbacks.
Not possible for zero-downtime deploys.
9. Compatibility and deploys
A backwards compatible service change can be
rolled out without problems.
Zero downtime!
Rolling back other services does not force
rolling back your service.
10. Compatibility and deploys
After (and only after) a service has rolled, can
clients upgrade.
Or perhaps just change a config flag
If the service needs to roll back, all clients must
roll back first.
11. Ways a ReST service can evolve
Changes to the API
For web services, this includes URIs, representations (i.e.
JSON), and HTTP methods
Changes to behavior
Most evolutions involve both kinds of change
13. Additional field in request
Make it optional (at least at first), with a
default value matching old behavior
@RequestMapping(value=”/petstore/pets”, method=GET)
public getPets() {
return petService.getAllPets();
}
@RequestMapping(value=”/petstore/pets”, method=GET)
public getPets(
@RequestParam(value=“species”, required=false)
Species species) {
return species == null
? petService.getAllPets()
: petService.getAllPets(species);
}
14. Additional field in request
Can also provide a default value:
@RequestMapping(value=”/petstore/pets”, method=GET)
public getPets(
@RequestParam(“species”) species)
@RequestMapping(value=”/petstore/pets”, method=GET)
public getPets(
@RequestParam(“species”) Species species,
@RequestParam(
value=”mustBeAdoptable”, defaultValue=”false”)
boolean adoptable)
15. Additional field in response
Most serialization libraries (including Jackson)
can ignore unmapped fields.
New clients can use the field value, while old
ones ignore it.
Ensure that the new field does not change
semantics of old fields!
16. New values in a request field
Pretty straightforward, provided that the
meaning of pre-existing values does not change.
Example – add a new value to an enum.
public enum Species {
DOG,
FERRET,
CAT
}
17. Narrowing set of allowed request values
Example – No longer allowing searches for
Ferrets.
Rarely happens
When it does, all clients must upgrade first
before the service can safely rely on the
narrowed set of values
18. Changing type
Of request field:
Make sure the old type can be parsed into the new type –
e.g. Integer to Double
Of response field:
Make sure new type can be parsed as old value – e.g.
Double to Integer
Alternative: add new field with new type, have
the old field “forward”
19. Changing type – forwarding field
{“age”: 12}
{
“age”: 12,
“ageAsDouble”: 12.3
}
20. New behavior
Changing business logic
Should be OK - this is why we do services in the first place!
Performing additional actions (or less actions)
If this breaks contract, then a new resource may be required
New failure modes
This is very similar to new values in response fields
21. Database backwards compatibility
Adding columns, tables, etc is OK...ish
They won't always get used!
Backwards population may be needed
Beware the generic update endpoint!
Removing: First update clients
If you know what they are!
Triggers and views can help here.
22. Safely adding a column (say, “foo”)
Problem: old client calls GET, followed by PUT.
Drops foo on the floor
Solution: BEFORE UPDATE trigger
IF :new.foo IS NULL THEN :new.foo = :old.foo
For inserts, a default value is simpler than a trigger.
23. Best Practices
Plan ahead for versioning changes
Keep your API as narrow as possible.
Document and test compatibility requirements.
Automated client tests.
Keep clients as up to date as possible.
24. Plan ahead for versioning changes
Always consider versioning issues when making
any changes to existing resources.
Do this up front, not as an afterthought.
Put care into initial API design.
Keep DTOs as simple as possible.
It is very hard to see which fields clients are using.
25. Keep your API as narrow as possible
You can't break what you never provided.
Splunk can show what is called, but not how
the results are used.
Narrow APIs often are also better tailored.
Avoid generic “update” endpoints if possible
Fields are part of the API – only include what
you need!
26. Document and test compatibility requirements
Make it clear which client versions are
expected to be able to talk to which service
versions.
Verifying these requirements should be part of
any service change.
27. Automated tests
Write automated tests which exercise the
service as a given version of a client would.
Tests for version n should be written before
starting version n+1, and ideally before rolling
version n to production.
Run old tests against new service version to
test backwards compatibility.
Copy and paste can be acceptable here.
28. Keep clients as up to date as possible
Once all clients have upgraded, any deprecated
fields, resources, etc. can be removed from the
service.
Maintaining backwards compatibility is
expensive, and gets more so with multiple
versions.
Cooperation is required.
29. What if you need to break backwards compatability?
1)Don't!
2)See #1
3)Talk with your colleagues – find a solution.
4)There is always a way, and it usually isn't that
hard.