I had a problem last week and it's about Web Services versioning. I read several posts about this issue but I didn't found a simple solution. Most solutions are somewhat complex using UDDI or proprietary based, but my problem was very simple stated like "Once published a Web Service you cannot change it in a way you will brake your clients". So, with this constraint how to evolve your application without break your API or getting too duplicated code?

Use URLs with wisdom

First develop a strategy for your services URLs, put the version of your service inside the URL. In my case:

  • http://myapphost/services/v[MAJOR]_[MINOR]/[SERVICE]

Here's a example:

  • http://myapphost/services/v1_0/SomeGoodService
  • http://myapphost/services/v1_1/SomeGoodService

Note that you should keep the name of the service, this is very important! Same service same name. You don't want SomeGoodService, SomeGoodImprovedService, SomeGoodImprovedFastService to be the names.

For the WSDLs files I use the same reasoning.

  • http://myapphost/wsdl/v1_0/SomeGoodService.wsdl

Or you can use dynamic WSDLs using functionality of the webservice framework, I use JBoss so I can provide a WSDL dynamically.

  • http://myapphost/services/v1_0/SomeGoodService?wsdl

Using a URL strategy is a good way to tell your client what version of your web service is in use, it's a fixed endpoint and remember that this URL will exists for a very long time.

Expose simple versions of your objects

When you have complex structures or dependencies on your objects this will be reflected inside your client code, so provide a simplification to your client.

Such simplification pays off, avoiding complexities of XML type mapping is good because you don't know in what language your client will be developed. Making deserialization easy is a good thing.

For a example in my current app I had a object using some joda time types, if I exposed such object to a web service lot of unnecessary complexity would flow into the client. To fix this I created a simple version of it, here's a simple method signature simplification:

 void setBeginHour(LocalTime beginHour);

to

 void setBeginHour(Calendar beginHour);

Some dirty details

Well, enough with recommendations (language and application server agnostic), now let's look into some real implementation. My environment is Java 5 with a JBoss 4.2.2 as application server. I will use a simple web service called AgeService, given a birth date it tells how years old is the client.

Here's the package layout of my web service:

  • br.com.svvs.model - Domain objects for internal use.
  • br.com.svvs.service - Web Service implementation package.
  • br.com.svvs.service.model - Simplified versions of domain objects.
  • br.com.svvs.service.util - WSTypeAdapter should translate domain to ws objects and vice-versa.

Now let's look into the Birth class:

package br.com.svvs.model;

import org.joda.time.DateTime;

public class Birth {

 private DateTime birthDate;

 public DateTime getBirthDate() {
   return this.birthDate;
 }

 public void setBirthDate(DateTime birthDate) {
   this.birthDate = birthDate;
 }

}

It's a very simple class, the problem is it uses DateTime from joda-time and I do not want expose such implementation detail. To hide this detail let's create a simplified version just for use on my service. Here's is the WSBirthV1_0:

package br.com.svvs.service.model;
import java.util.Calendar;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;


@XmlRootElement(
   name="wsBirth",
   namespace="http://wsbirth_1_0.model.service.svvs.com.br"
)
@XmlType(name="wsBirth")
public class WSBirthV1_0 implements WSBirth {

 private Calendar birthDate;

 public void setBirthDate(Calendar birthDate) {
   this.birthDate = birthDate;
 }

 public Calendar getBirthDate() {
   return birthDate;
 }

}

WSBirthV1_0 changes the signature to a simple Calendar object which most XML mappers know how to convert. Note WSBirthV1_0 implements WSBirth, this a marker interface we will use it in WSTypeAdapter, let's look at the code:

package br.com.svvs.service.util;

import org.joda.time.DateTime;

import br.com.svvs.model.Birth;
import br.com.svvs.service.model.WSBirth;
import br.com.svvs.service.model.WSBirthV1_0;

public class WSTypeAdapter {

 public static <T extends WSBirth> T convertTo(Class<T> type, Birth wsbirth) {
 
   String typeClassName = type.getSimpleName();
 
   if(typeClassName.equals("WSBirthV1_0")) {
     return type.cast(convertToWSBirthV1_0(wsbirth));
   } else {
     throw new RuntimeException("Don't know how to convert " + typeClassName);
   }      
 }

 private static WSBirthV1_0 convertToWSBirthV1_0(Birth birthDate) {
   WSBirthV1_0 wsbirth = new WSBirthV1_0();
   wsbirth.setBirthDate(birthDate.getBirthDate().toCalendar(null));
   return wsbirth;
 }

 public static <T extends WSBirth> Birth createFrom(T wsbirth) {
 
   String versionName = wsbirth.getClass().getSimpleName();
 
   if(versionName.equals("WSBirthV1_0")) {
     return createFromWSBirthV1_0((WSBirthV1_0) wsbirth);
   }
   else {
     throw new RuntimeException("Don't know how to produce Birth from " + versionName);
   }  
 }

 private static Birth createFromWSBirthV1_0(WSBirthV1_0 wsbirth) {
   Birth birth = new Birth();
   birth.setBirthDate(new DateTime(wsbirth.getBirthDate().getTimeInMillis()));
   return birth;
 }

}

In this class, using generics, we can provide a simple API to convert to and from ws objects. Note the use of our marker interface WSBirth. It's a utility class with static methods, we can grow our private methods for more and more versions. And at last, here's the service:

package br.com.svvs.service;

import javax.ejb.Stateless;
import javax.jws.WebService;

import org.jboss.wsf.spi.annotation.WebContext;
import org.joda.time.DateTime;
import org.joda.time.Interval;

import br.com.svvs.model.Birth;
import br.com.svvs.service.model.WSBirthV1_0;
import br.com.svvs.service.util.WSTypeAdapter;

@Stateless
@WebService(
   name="AgeService",
   serviceName="AgeService",
   targetNamespace="http://age_1_0.service.svvs.com.br"
)
@WebContext(
   contextRoot="/services",
   urlPattern="/v1_0/AgeService"
)
public class AgeService_1_0 {

 public String tellAge(WSBirthV1_0 wsbirth) {
 
   Birth birth = WSTypeAdapter.createFrom(wsbirth);
 
   System.out.println(birth.getBirthDate());
 
   Interval i = new Interval(birth.getBirthDate(), new DateTime(System.currentTimeMillis()));
 
   return "You have " + i.toPeriod().getYears() + " years old.";
 }

}

Discussion

At first it seems to much work but I disagree. When you have to maintain old web services and did not planned for code evolution this could raise much more work, usually with a copy and paste solution (happened to me).

Some good points about this solution:

  • You can evolve your code without fear of breaking the old service, just follow the conventions.
  • Isolated conversion from domain to ws objects, you don't forward ws objects inside the domain.
  • Control of what is exposed and how, simplified client development.
  • It works.

Some bad points:

  • Messy names like ServiceV1_0, ServiceV1_2, WSObjectV1_0...
  • Care should be taken on annotations and namespaces.
  • If you have complex domain objects, you have to create a simplified version of each object used in your service.
  • Not simple enough, I still think that could be simpler.