Service-Oriented Architecture (SOA) can greatly accelerate application development, namely through reusable services and through applications that require less new code because they can rely on those reusable services. SOA can also greatly complicate application development, as teams need to work concurrently on different parts of the application and yet successfully put the pieces together at the end. This paper explores what makes SOA development difficult and offers a process to simplify it. With this process, an organization can greatly increase its success with SOA.
Developing an application using SOA gives the application more deployment options, but also makes development more difficult. This is because SOA splits the application into two distinct parts:
- SOA Service Provider (SOA-SP) -- At this tier, code implements the services. The code has a service API that declares the services and provides the means for clients to invoke those services.
- SOA Service Coordinator (SOA-SC) -- At this tier, code provides user functionality that is implemented using services in one or more SOA-SPs. It probably has a UI or GUI so the user can interact with the SOA-SC as a traditional application.
For example, the SOA for a financial application might look like the one shown in Figure 1.
Figure 1: Personal finance application and services
Usually, separate teams are responsible for developing the two separate parts concurrently. One team is responsible for developing the SOA-SC -- what the user thinks of as the application. Another team is responsible for developing the SOA-SP, perhaps multiple teams each developing a few services. While some providers may already be implemented, others may need to be developed especially for this application.
Which brings us to the first problem: How can the coordinator team develop its part of the application if some of the providers are not yet implemented?
The two teams -- the team developing services and the team developing the coordinator -- need to synchronize their activities. They must agree on the service API; it may be as simple as a Java™ interface or Java Message Service (JMS) message formats and queue names, but nevertheless has to be agreed upon. But interface is not enough; a service has behavior, so the teams must agree on how the service will behave. A service will not always work successfully, so the teams must agree on error scenarios and appropriate responses.
In an ideal world, teams work together easily, initial decisions can be revised later with minimal impact, and estimates can be flexible and evolving. In the real world, teams often have trouble working together, unbending decisions tend to be made early (even prematurely), and estimates are binding.
Hence the second problem: How can the coordinator and provider teams come to an early yet reliable agreement of how the service will work?
A service is often made available redundantly by multiple providers. This offers load balancing and failover amongst the providers, making the coordinators' experience with the service more consistent and reliable. The redundancy may be achieved by deploying the same implementation of the service multiple times. When different providers are deployed by different vendors, however, the services will almost certainly have different implementations. Nevertheless, all the implementations of all the providers must work the same for the coordinators, so that a coordinator can invoke the providers interchangeably. The different implementations of the service, especially from different vendors, are developed by different teams, yet the teams must coordinate to make sure they're developing the same service.
Therefore the third problem is: How can multiple provider teams implementing the same service make sure their implementations are compatible?
These are some of the main problems of developing with SOA. Coordinators must be developed using services that are not yet developed, coordinator teams and provider teams must agree on how the service is going to work, and multiple provider teams must agree on how the service is going to work. Without a good process, this is a recipe for chaos which, left unchecked, will doom a SOA project to failure.
Here is a simple process for synchronizing coordinator and provider teams so that the development of reusable services and the applications that consume them will be successful:
- Describe the services with service use cases.
- Develop service tests that embody the service use cases.
- Develop service mocks that pass the service tests.
- The provider teams employ the service mocks as prototypes and the service tests as requirements to implement the services.
- The coordinator teams employ the service tests as examples of what the service consumers can do and the service mocks to test their code while the real services are still being developed.
This simple process addresses how to scope out the services and how to keep the teams in agreement and productive so as to avoid unwelcome surprises. To be fair, there are a number of other issues this process does not address. It does not address how the services themselves are developed or how the coordinator applications are developed. It does not address quality of service issues for how robust the service is, but simply whether the service provides the necessary behavior. Nor does the process address more generally how traditional monolithic applications are rearchitected using SOA or how services are discovered or designed. All that is necessary as well, but it is beyond the scope of this process.
This process gets the coordinator applications working with the service implementations while enabling both parts to be developed concurrently by teams working fairly independently. That's not everything a SOA project needs, but it's a pretty good start.
To illustrate this process, I consider how to implement a simple service. The example is everyone's favorite service example, a stock quote service. To jazz it up a little, I make the following three types of information available:
- A simple quote of the current price
- A complex quote containing the current price, high and low price for the day, and volume for the day
- A historical quote containing a complex quote for a date in the past
This example should be sufficient to illustrate how the process works.
The first step in this SOA development process is to develop service use cases that describe the services.
Alistair Cockburn defines a use case as "a contract between the stakeholders of a system about its behavior" (see his book, Writing Effective Use Cases, listed in the Resources section). The use case must fit within the scope of the system being defined, represent the point of view of the primary actor using the system in this case, and express the actor's use of the system at a consistent level of abstraction.
One example Alistair gives is Buy Stocks over the Web, where a purchaser uses a personal finance application that works with a stock broker's Web site to purchase stock. The scope includes both the finance application and the broker's Web site. The purchaser is the primary actor. The level of abstraction is the interaction between the application and the Web site, but not the details within the application or the Web site. The use case describes the main success scenario, according to which the purchaser buys the stock, and several extensions for what could go wrong.
So a use case is a text description of how you want the system to work, who's involved and how they interact, how it all works when things go right, and what should happen when things go wrong. It concerns what the system will do, but not how that will be implemented.
Now imagine that instead of a system or application, you're talking about an SOA service. The use case technique still applies. The technique can be employed to describe how the service consumer interacts with the service provider, explaining what the service does without describing how it is implemented. Initial drafts of a service use case should focus on how the service behaves. Because this is a service that must be invoked, later drafts of the use case should also specify the invocation protocol -- the technology, transport, and data formats that will be used to invoke the service. (Use case purists might say that even protocol is an implementation detail that doesn't belong in a use case, and they'd be correct. But a service use case describes not only a service but also how to invoke that service, so protocol is part of the contract between the consumer and provider actors and must be specified somewhere.)
Thus the first step is to develop use cases to adequately describe what the services do. They represent the consumers' requirements for the behavior the providers must supply, and they are created primarily by the coordinator development teams but with input from the provider development teams as well. Both kinds of development teams must be satisfied with the use cases, because those are the requirements from which all of the teams develop their parts of the application.
It's important for a service not only to work properly under good conditions, but also to handle error conditions properly. Thus your service use cases should address error conditions and bad input that the service cannot process successfully. Many of these error pathways show up as alternate paths in the use cases. Other error scenarios may be so extreme that they require their own failure use cases. In both approaches, the use case must document how the service handles the error just as thoroughly as the successful paths.
For example, consider the service use cases for the stock quote example. It needs to do three things, so it requires the following three service use cases:
- Simple Quote: The consumer passes in a stock symbol; the provider returns the current price for the indicated stock.
- Complex Quote: The consumer passes in a stock symbol; the provider returns the current price and the current day's high, low, and volume for the indicated stock.
- Historical Quote: The consumer passes in a stock symbol and a date; the provider returns a complex quote for the indicated stock and date.
Even for such a simple example, there are a number of issues that need to be decided and added to the use cases, as follows:
- What if the stock symbol isn't valid or is for a stock not supported by the provider's exchange?
- What format should be used for the price? Floats can have round-off error. Decimals are more precise but not as standard. Strings are less efficient but less ambiguous.
- What format should be used for the complex quote? Comma separated values? An array? An object? An XML document? A SOAP response?
- What if the requested date is today or in the future? What if it is for a date in the past that the market was not open? For a date before the stock started trading on the exchange? A date so long ago that records no longer exist? What if the stock's symbol or exchange has changed since then?
Even developing simple use cases for this paper isn't so simple. Use cases are tricky and care must be used to develop them well. That care is a good investment; good service use cases lead to good service tests and service mocks that help keep development teams on track.
The second step in this SOA development process is to develop service tests that codify the use cases in executable form. The test will only pass when the services properly implement the use cases.
Kent Beck specifies that a test should be automatic, isolated, and should verify things that might break (see the reference to his books Extreme Programming Explained and Test Driven Development in the Resources section). Testing -- using tests to develop working software -- is one of the twelve practices that define Beck's methodology, called extreme programming (XP). It is the heart of test-driven development (TDD) -- how you would perform XP if you could only follow a single practice. When you follow XP and TDD, you develop the tests first, then develop software to make the tests pass, and finally repeat those steps until the software is complete enough.
What should you test? The ideas for tests can come from a number of places, but use cases are an ideal source of inspiration for tests. Use case text and diagrams describe the users' understanding of the requirements. Tests express that understanding in a much less ambiguous form, as code that executes reliably and repeatedly. Use cases and tests are counterparts that express the same ideas in different forms for different audiences (man and machine).
Service tests for service use cases are no different, although they may be thought of more as functional tests than unit tests. Service tests don't verify how the services are implemented; the provider development teams can implement their own unit tests for that purpose. Service tests verify that the services provide the behavior the service use cases say they should. The tests need to test the error pathways as well.
The tests end up defining the service's expected interface. This interface is typically a Web Services Description Language (WSDL) file for a Web service, a Java interface, or EJB remote interface for a Java component, and so on. While it might seem simpler to develop the interface first and then implement the tests against them, the more direct approach is to develop the tests first, then develop the interfaces that enable the tests to compile successfully.
Service tests can be developed with a simple unit testing framework like JUnit or Cactus. The framework pretends to be a consumer of the service and does what consumers would do. The following are some potential tests:
- Invoke
simple quotewith IBM and verify that you get back "$100.00." - Invoke
simple quotewith MSFT and verify that you get back "$30.00." - Invoke
simple quotewith BOGUS and verify that you get back aninvalid stock symbolerror.
The tests for complex quote and historical quote would be similar. There should also be tests for possible infrastructure errors, such as remote exceptions and HTTP 400 errors. In the end, the tests should verify everything specified in the service use cases; specifying an action in a use case but then not checking it in one or more tests means that the consumers can't expect the providers will actually perform that action.
The third step in this SOA development process is to develop service mocks -- mock objects that pass the service tests. These service mocks are simple prototypes of the real service providers.
Kent Beck describes a mock object as a testing object, one that implements a fake version of an expensive or complicated resource by answering constants. For example, a mock database is a simple object with the API of a database that accepts a few known SQL strings and returns a fixed set of results for each one. A mock object enables you to test your component without having to depend on the external resource.
Now imagine that the external resource is a SOA service. If your component uses the service, when you test your component you're testing the service as well. If the service isn't working right, or is unavailable, your tests will fail even if your component is working just fine. If the service is slow, as is usually the case when invoking a service remotely over a network, then your tests will be slow -- this will deter you from running your tests as frequently as you should. And if the service hasn't been implemented yet, then you won't be able to test your component at all.
So a good approach is to develop service mocks, mock objects that are simple emulators of real services. The service mock has the same API as the real service; it implements the interfaces developed for the service tests. How should the service mock work? It should pass the same service tests you've already developed, which shows that the mock really works the way the real service does.
In some respects, a service mock can actually be more useful for testing than a real service is. Say your component is using a service that returns stock quotes. If you pass in the symbol IBM, what answer should you get back? $50? $100? $150? Depends on what the current stock price is, but that's a chicken-and-egg problem for testing. With a service mock, you hard-code the mock to always return $100, then test against that, which is actually more reliable than testing the real service.
Who develops the service mocks? The provider teams should develop them, not the coordinator teams. They represent a quick implementation of what the provider teams plan to implement for real. If there are multiple provider teams for the same service, they must coordinate to produce one mock service that they all agree upon.
This example service mock needs to pass the example service tests I wrote earlier. So its implementation of simple quote is a case statement. If the service is just a plain old Java object (POJO), the mock would be a special implementation of a common interface like the following:
Listing 1: Service mock that is a special implementation of a common interface
public class StockQuoteMock implements StockQuoteService |
Then simple quote would be a method declared in StockQuoteService and implemented in StockQuoteMock something like the following:
Listing 2: Simple quote method declared in StockQuoteService and implemented in StockQuoteMock
public String getSimpleQuote(String symbol) throws InvalidSymbolException {
if (symbol == null) throw new InvalidSymbolException(symbol);
if (symbol.equals("IBM")) return "$100.00";
if (symbol.equals("MSFT")) return "$30.00";
if (symbol.equals("BOGUS")) throw new InvalidSymbolException(symbol);
throw new InvalidSymbolException(symbol);
}
|
If the service is something more complex, like a stateless session bean or a SOAP Web service, this POJO code still serves as the basis for the more complex mock implementation. In any event, the mock implementation should definitely not try to handle every possible stock symbol or access a database with real-time data. The mock implementation should be just enough to pass the service tests.
The fourth step in this SOA development process is for the provider development teams to implement services that pass the service tests.
At this point, the provider teams are already well on their way toward developing the service. How can this be, since they haven't begun implementing the service yet? Well, they've developed service use cases that describe how the service should work, and developers being developers, this means that they've already begun to think about how to implement the services. The developers have already created service tests, which show what the services' API will be and help demonstrate the services' behavior. And they've developed service mocks, which are quick prototypes of how the real service will work.
So the developers already have a pretty good idea of how to implement the service, even though they haven't yet implemented a single line of code for it.
This almost goes without saying, but as the developers implement the service, the service must pass the service tests. How will they know when they've finished implementing the service? The service has been implemented completely when it passes all of the tests. During development, their development activities may make the team think of additional functionality to test. Rather than lose those insights, a disciplined team will capture and add them to the test suite for the service. The service implementation must pass those as well. The provider team should also communicate those additional tests to the other provider teams and to the coordinator teams, so that all of the teams are in sync with the same set of tests.
Ideally, the provider team will be able to successfully implement services that pass the tests without having to modify the agreed-upon tests. Yet often this turns out to be impractical. As the developers implement the service, they sometimes find that they need to change the service's interface or behavior. If the service tests are good ones, and the developers change the way the service works, then the tests won't pass anymore. For the testing to remain adequate, the developers have to modify the tests to verify the new design. Changing the tests means that the service mocks now don't pass the tests, so they must be changed as well to accurately mock the way the service now works.
If the provider developers change the service tests or the service mocks, they need to notify the coordinator developers, and the developers of any other providers of that service, as soon as possible. Anyone using the old tests and mocks are developing to an agreement that is now out-of-date, so the teams must resynchronize around the new tests and mocks. If the other teams resist accepting the new tests and mocks, then resynchronizing becomes a point of renegotiation between the teams. Hopefully they still agree on the service use cases, so they can start again from there, developing an agreed-upon set of tests and mocks.
Example provider implementation
The provider development team would develop a class or component that implements StockQuoteService, works like StockQuoteMock, and makes the stock quote tests pass. Whereas the mock is a simple object with hard-coded responses, this provider is a component that provides real behavior. The implementation should perform the following actions:
- Support all valid stock symbols, at least for the provider's stock exchange.
- Make use of a database that contains real time prices for the supported stocks.
- Convert the price format the database uses into the format the service returns.
- Convert the database's result for an invalid symbol, such as an empty query result, into the error the service expects.
- Implement the service's protocol, such as an EJB remote interface, HTTP Web service, or JMS request and reply messages.
Because the mock and the real provider implement the same interface -- in this example the Java interface StockQuoteService -- the service tests can use either implementation; just configure the test with the right class to instantiate. To run the tests, you also need to configure the test database with the stock prices the tests expect.
The fifth step in this SOA development process is for the coordinator development team or teams to implement applications that use the services. The applications use the service mocks until the real service implementations are ready.
At this point, because they have an arsenal of service mocks, a coordinator team can proceed as if the service providers are already implemented and available. Furthermore, the coordinator team not only has a set of services ready to use (in other words, the mocks), they also have a set of tests that demonstrate how the services work and how a client might make use of them. The team can use these tests as a simple prototype of how they might implement their coordinator. Like the provider teams, the coordinator team is already well on its way to implementing the coordinators even though they haven't yet implemented any code.
Ideally, the coordinator team will be able to successfully implement their coordinators using the agreed-upon service mocks. Yet sometimes this can turn out to be difficult. The mocks do not provide some needed behavior or the desired interface. The coordinator may also need individual services that are finer grained than what the mocks provide. If a coordinator needs additional functionality, it can attempt to implement the functionality itself. If the coordinator desires a different interface, it can attempt to implement an adapter that converts the interface it wants to the interface the mock implements. If the coordinator wants finer-grained functionality, the team needs to modify the mocks and their tests.
These changes drive a need to resynchronize with the provider teams. Let's say the coordinator team implements additional functionality or different interfaces to make the service useful. If that additional behavior is not specific to the coordinator and general to the service, then the additional behavior can potentially be reused by other service consumers. It should thus be built into the providers. The needed changes to the providers can and should initially be modeled as changes to the mocks and their tests. When the coordinator team must modify the mocks and their tests -- either for additional functionality or for finer-grained functionality -- then these changes must be made to the providers as well for everything to work. The changed mocks and tests become a point of renegotiation between the coordinator team, the provider teams, and other coordinator teams. They must resynchronize on a newly agreed upon set of mocks and tests.
Example coordinator implementation
The coordinator development team would implement a client component that delegates to an implementation of StockQuoteService. It would act like the service tests do, except it would use the service to provide real functionality to a GUI or a client application. The coordinator implementation could only use the functionality in the StockQuoteService proven to be available by the service tests. The Java compiler ensures that the coordinator code can only invoke the methods declared by the service interface; keeping the coordinator implementation consistent with the tests' implementation ensures that the service works as expected.
So how does the process work in practice?
First, develop the service use cases. The service use cases team can consist of representatives from the provider and coordinator teams. Or it may be made up solely of analysts who specialize in requirements gathering and use case development. Whereas traditional use case development focuses on how people use applications, this team must focus on how components integrate. Their focus should not be on how the providers will be implemented, nor on how the coordinators could be implemented. Rather, they should focus on what the services are, what they do, and how they're invoked.
Second, codify the service use cases into service tests. Whereas use cases are human readable, service tests represent the same requirements, but in a form that is machine executable. These tests must be implemented by developers, not by the analysts that may have developed the use cases. The test developers can be members of the provider and coordinator teams, or whoever is available and qualified to implement tests. Before the tests can be considered final, a representative of each team must approve them, demonstrating that all teams are in agreement regardless of who developed which tests.
Third, develop service mocks that pass the tests. The team that develops the tests usually implements the mocks as well. The mocks demonstrate that the tests can pass, serves as prototypes for the provider teams, and enables the coordinator teams to proceed with development. Like the tests, the mocks cannot be considered final until all of the teams demonstrate their agreement by approving the mocks. In other words, one set of teams cannot force a set of tests and mocks on another set of teams; everyone must agree or, sooner or later, chaos will ensue.
Fourth, the provider teams develop providers that act like the mocks and pass the tests. If and when they add to the tests and the mocks, and especially if they change the tests and the mocks, they must distribute these changes to the other teams and resynchronize. They cannot force these changes onto the other teams; all teams must agree.
Fifth, the coordinator teams must develop coordinators that work properly using the mocks. If and when they need to change the mocks, they need to update the tests as well. They then need to distribute their changes to the other teams, and all of the teams must find a point of agreement -- a common set of tests and mocks -- to resynchronize against.
The steps really do come together to form a simple development process.
So does this process solve the problems I stated at the beginning?
1. How can the coordinator team develop its part of the application if some of the providers are not yet implemented?
The mocks solve this problem. They can be developed quickly. While development of the actual providers takes much longer, the coordinator teams can concurrently develop the coordinators using the mocks. This works very smoothly as long as the following conditions are met:
- The mocks are thorough, meaning that the tests and the use cases they codify are thorough.
- None of the teams has to change the mocks. Whenever a team has to change the mocks, they should resynchronize as soon as possible, before they get any further out of sync.
2. How can the coordinator and provider teams come to an early yet reliable agreement of how the service will work?
Because the tests and mocks can be developed quickly, and because they're real code that really runs, they provide early validation that the use cases make sense and that the teams are indeed in agreement. With experience, the more effort that can be put in up front to ensure the thoroughness of the tests and mocks, the less likely those artifacts will need to change and the less resynchronization that will be needed during the rest of development.
3. How can multiple provider teams implementing the same service make sure their implementations are compatible?
A common set of tests, along with the mocks that make them pass, serve as a common frame of reference to make sure independent provider implementations are nevertheless compatible and interchangeable. The more thorough the tests are up front, the less they'll need to change, and therefore the less the teams will need to resynchronize.
So this process solves the major development problems of multiple teams implementing parts of SOA.
This paper covered the following:
- Common development problems that concurrent and independent teams can run into attempting to develop an application with SOA.
- A simple five-step development process which solves these problems using service use cases, service tests, and service mocks.
You can use this process in your organization to greatly improve your success with SOA.
Note: The author would like to thank Dave Artus, Kyle Brown, and Ben Thurgood for reviewing this article.
Learn
- Writing Effective Use Cases -- by Alistair Cockburn.
- Extreme Programming Explained -- by Kent Beck.
- Architecture: Build for the future -- visit the Architecture area on developerWorks and get the resources you need to advance your skills in the architecture arena.
- Test-Driven Development -- by Kent Beck.
- JUnit Testing Framework -- written by Erich Gamma and Kent Beck.
- Cactus Testing Framework -- an Apache Jakarta Project. The Cactus project enables easy testing of your server-side code.
- Service-Oriented Architecture expands the vision of Web services -- learn how to move forward from simple models to those that represent real-world business models of arbitrary complexity (developerWorks, April 2004).
- Improve your SOA project plans -- turn any SOA engagement into a success with the help of these governance principles (developerWorks, July 2004).
- Information management in Service-Oriented Architecture -- discover the role of information management in SOA (developerWorks, March 2005).
- Reuse engineering for SOA -- explore the inhibitors to software reuse as they apply to SOA and learn how reuse engineering can make a positive impact in realizing the value of SOA (developerWorks, September 2005).
- Service-Oriented Architecture -- find informational articles in the IBM Systems Journal.
- Enterprise Integration Patterns -- learn how to design, build, and deploy messaging solutions.
- SOA and Web services -- hosts hundreds of informative articles and introductory, intermediate, and advanced tutorials on how to develop Web services applications.
Discuss
- The Parts of an SOA Application -- blog by Simon Johnston.
- Service Oriented Architecture and Business-Level Tooling -- blog by Simon Johnston.
- The Two Parts of an SOA Application -- blog by Bobby Woolf.

Bobby Woolf is a WebSphere J2EE Consultant for IBM Software Services for WebSphere (ISSW). Bobby assists clients in developing applications for WebSphere Application Server using WebSphere Studio Application Developer. He is a co-author of Enterprise Integration Patterns and The Design Patterns Smalltalk Companion. He also has a blog on the IBM developerWorks Web site called J2EE in Practice. Bobby is a frequent conference speaker.
Comments (Undergoing maintenance)





