Tuesday, July 15, 2008

SOA realization: Service design principles

Introduction

Service-Oriented Architecture (SOA) offers a vision of IT flexibility enabling business agility. In this article we will focus on two specific aspects of IT flexibility: decoupling and ease of process implementation. How well individual services are specified and realized has a significant impact on these aspects of IT flexibility, and hence on business agility. Our goal here is to provide guidelines for specifying and realizing services that enable the SOA vision. We use the following structure:

  • First, we describe the context within which services and service operations will be specified and realized. We consider the responsibilities of the SOA infrastructure and the responsibilities of the service.
  • Next, we consider the design principles that apply to the specification of services as a whole rather than to individual operations.
  • Finally, we state the design principles that apply to individual service operations.

The design principles we outline are intended to promote IT flexibility by increasing both decoupling and ease of process implementation, so we will complete our introduction by examining these ideas in more detail.

Decoupling

SOA principles place a strong emphasis on decoupling service consumers from service providers, and there are useful informal agreements about what such decoupling actually means. One underlying concept behind decoupling is that changes to the service provider should not necessitate corresponding changes in the service consumer. For example, a decision to redeploy a service currently running on a particular operating system to a different platform could reasonably require no changes to the services consumer. A major guiding SOA principle is to reduce dependencies between consumer and provider.

Applied at the technical level, decoupling emphasizes technologies such as Web services and asynchronous message delivery that enable consumers to make choices of implementation and availability independently of the service provider. We would also expect the SOA infrastructure to enable technical decoupling in a variety of ways, for example:


Table 1. Decoupling techniques
DependencyDesired decouplingDecoupling technique
PlatformChoice of service hardware, operating system, or implementation language should not constrain choices for service consumer.Services employ standard service exposure mechanisms that do not constrain service consumer platforms. Commonly, Web service technologies are used.
LocationChanges to service implementation locations should have minimal impact on clients.The SOA infrastructure offers runtime service location and request routing mechanisms.
AvailabilityIndividual service availability characteristics should not impact overall availability of business processes. For example it should not be necessary to synchronize maintenance schedules for services and consumers.The SOA infrastructure offers reliable asynchronous invocation mechanisms with suitable store-and-forward characteristics.
VersionsIt should be possible to introduce new versions of services without requiring simultaneous upgrades to all service consumers.The SOA infrastructure provides message transformation and enrichment capabilities. The service consumer accesses a consistent interface offered by the infrastructure even though the service provider interface itself has changed, the infrastructure mediating between consumer and provider.

A service should be designed so that it is is compatible with the SOA infrastructure into which it will be deployed, and in particular the service should ensure that unnecessary coupling is avoided. As a counter-example, a stateful service interface will tend to increase coupling between consumer and provider by associating a consumer with a particular provider instance.

The concept of decoupling also applies at a nontechnical, business level. The service consumer should be insulated as far as possible from the details of the business logic implemented by the service provider. Again, careful service design is required in order to achieve this decoupling. In the section Service Design Principles, below, we will consider the benefits of expressing the service interface in terms of meaningful business operations rather than fine-grained primitive methods.

Process Implementation

In an SOA we implement business processes by choreographing individual services, either programmatically or by using tools based on Business Process Execution Language (BPEL). If we are to realize the SOA vision, then we must simplify the task of creating and modifying processes -- that is, the task of service choreography.

The goal of choreographing a business process is to accurately and efficiently implement the desired business logic. Among the problems faced by the process implementer are the following:

  • Selecting appropriate service operations
  • Arranging to invoke the services with correct parameters
  • Handling the various possible response, including error responses

It should be clear that the quality of service design can have a significant impact on the ease of choreography. Decisions about the names and number of services, operations and parameters, the quality of documentation and the degree of interdependence between service operations -- all design issues -- can have a significant impact on choreography.





SOA design principles

The guidelines in this section are concerned with the overall SOA system, representing decision points for establishing your system. What prescriptions and guidelines will you give the service designers and implementers? What capabilities will your SOA infrastructure offer? We give a number of suggested design principles, but each represents an engineering trade-off. Your enterprise may have specific requirements that lead you to make different choices from our general recommendations. Our intention in setting out our design principles is to identify the decision points; the responsibility for those decisions lies with the architects. We do not claim that our set of design principles is exhaustive; it is very likely that as you implement an SOA in your enterprise you will adopt additional principles, and we would be very interested to hear about them.

SOA requires consistency

There are many candidate technologies for creating, publishing, discovering, and invoking services. An SOA should provide a reference architecture specifying particular mechanisms that service providers and consumers will use; we should aim for consistency across all participants in the SOA. Such consistency should reduce development, integration, and maintenance effort.

If there are needs for exceptions to the reference architecture, we prefer a complementary approach. For example, suppose that UDDI were the chosen mechanism for service publication and discovery, but a particular development team had a well-established development process based on some other repository technology? We would choose to invest effort in publishing that team's services to both repositories. Hence existing consumers of the service could use their familiar -- if nonstandard -- repository. Consumers operating in the common SOA infrastructure would be able to use the standard --perhaps UDDI -- repository for all services.

SOA simplifies development

We would expect any enterprise-scale SOA infrastructure to be both scalable and resilient; it should also include industrial-strength Enterprise Service Bus (ESB) and security technologies. Or, to put that another way, developers of services and processes targeted at the SOA infrastructure exploit sophisticated middleware, relying on the SOA infrastructure to provide solutions to problems such as authentication, message transformation, and reliable message delivery.

One important principle should underlie the provision of these middleware capabilities: service and process developers should be insulated from the complexities of the middleware implementation. Our idealized goal is that developers working in our SOA environment should need only business domain knowledge and basic programming language skills.

We can work towards this goal in a number of ways, as follows:

  • Declarative techniques: The J2EE programming model is an example of using declarative techniques to provide separation of concerns between the application logic and the configuration of middleware. For example, an application assembler applies security authorization of roles for EJB methods by adding entries in deployment descriptors rather than in code; then a deployer will map the roles to users and groups. Note that the developer need write no authorization code.
  • Abstractions: In some cases the SOA infrastructure may offer APIs for particular purposes. For example, the SOA infrastructure may have error reporting or auditing facilities. We should take considerable care in designing these APIs, paying attention to ease of use. We should favor declarative techniques over programmatic configuration of these facilities. Also, where standard APIs are available, for example the Java logging APIs, we should expose the SOA infrastructure capabilities via those standard APIs rather than devise our own.
  • Code generation: Where some complexity of code is unavoidable it may be possible to use code generation techniques. As an example, consider Web Services Definition Language (WSDL), which can be used to hide complexities of SOAP, HTTP and JMS from the developer. This is accomplished through the combination of machine-readable interface definitions expressed in WSDL and tooling that can generate language-specific implementations of the relevant invocation code from the WSDL.
  • Tooling: In cases where details of the SOA infrastructure unavoidably intrude into the developer's code, we can reduce complexity for the developer by extending the development environment with suitable tooling. An Eclipse-based environment offered by the IBM Rational® Software Development Platform products is readily extensible with custom plug-ins, code snippets, and user guides.
  • Model-driven development: Model-driven development techniques can be seen as a particularly sophisticated combination of the previous two approaches, exploiting both tooling and code generation capabilities to simplify the development experience. The developer produces Unified Modeling Language (UML) models that can be transformed to code that includes the code necessary to exploit the SOA infrastructure.

In summary, in defining a Service-Oriented Architecture and its infrastructure, we must pay careful attention to the needs of developers. Where we offer developers guidelines for how they should develop or use services we should seek mechanisms to encourage adherence to those guidelines. A general principle should be "Doing the easy thing is doing the right thing." In other words, adherence to the guidelines should be perceived as taking the line of least resistance. Governance within the SOA becomes a crucial element to its success.

From the developer's perspective there is a responsibility to understand the SOA infrastructure and guidelines and to work with the guidelines rather than seek to avoid them.

Services have standard, formally defined, machine-readable interfaces

Having established that tooling and code generation can play a significant role in SOA implementation, we can now emphasize the importance of using machine-readable interfaces. When we describe interfaces using a well defined machine-readable language, we enable a broad range of tooling capabilities. Remembering that we wish to promote decoupling, we strongly favor the use of formally-defined open standard languages such as WSDL over a proprietary format.

The concept of a machine readable approach should be expanded from the description of service interface, such as WSDL, to all other forms of declarative information or meta-data. This twin emphasis on declarative techniques and machine-readable metadata is what pushes the complexity away from the business application developer into the standards-based middleware. Technologies such as the emerging WS-Policy are an important enabler of this approach.

Services are designed for reuse

Service designers should keep in mind that any service they product can potentially become a reusable asset. The designers should not exclusively focus on requirements of the initial consumers of a service, but rather should undertake more extensive business analysis in order to determine more complete requirements. We recommend that designers consider the potential evolution of a service:

  • The design must accommodate increasing throughput; if a service is successful then the number of consuming services may well increase and hence usage may well increase by orders of magnitude.
  • If the number of consuming services increases then both data volumes and concurrent data access patterns may be significantly different from the initial situation.
  • We must anticipate that there will be requests for extensions to the service; new consumers may well need additional functionality and modifications to existing functionality

Many of the design principles we discuss in the remainder of this article are particularly relevant to ensuring the scalability and maintainability of services. We offer one word of caution: it is possible to spend undue effort in designing services for potential reuse and hence to "over-engineer" implementations. We would encourage an initial focus on the design of the service interface, ensuring that it enables scalability; our design principles should help with that. Then produce a tactical implementation of that interface sufficient to meet current, known requirements. Provided that the interface is well-designed it should be possible to substitute a more scalable implementation when the need arises.






Service design principles

Remembering that a service is a logical grouping of service operations whose interface is published in some agreed-upon format, we will now discuss design principles that apply to the service as a whole. We will discuss the design of individual operations in the section Service Operation Design Principles, below.

Services are named to maximize consumability

We have one guiding principle in the choice of names for services, operations, data types and parameters: we want to maximize the consumability of our service. We want to help process developers identify the services and operations that they need to use to implement the business processes. We therefore strongly recommend the use of names that are meaningful in the domain of expertise of the service consumer, favoring business concepts over technical concepts.

To refine our recommendation: services should be named using nouns, operations should be named using verbs.

Compare the two service definitions shown inLISING 1 and LISTING 2. We use a simplified pseudocode to reduce programming language "clutter".


Listing 1. Service definition using verb phrases and IT constructs

ManageCustomerData {

InsertCustomerRecord()

UpdateCustomerRecord()

// etc ...
}


Listing 2. Service definition using noun and verb phrases and Business concepts

CustomerService {

CreateNewCustomer()

ChangeCustomerAddress()

CorrectCustomerAddress()

EnableOverdraftFacilityForCustomer()

// etc ...

}


Note how the definition in LISTING 1 is expressed in terms of IT concepts, and uses verb phrases for both service and operations. In LISTING 2, the service is expressed as a noun and the operations are named using verbs in phrases with clear business meaning. We contend that the second example is more consumable. Additionally, in the second example, the business intention of the service is clear, not just the outcome. So, rather than "update customer record" (which could be any update for any reason), we specify "enable overdraft facility." Likewise, we have a method to "change customer address" when a customer has moved, or "correct customer address" when we wish to correct invalid data, as it is conceivable that these two actions could have different service logic.

If our service and operation names are to be expressed in terms of business concepts we must pay careful attention to how those names are to be determined. There is a strong need for a formal glossary of accepted terms, which would be the product of a business analysis activity. The glossary should have a formal owner.

Services have well-chosen granularity

The word granularity is used in a number of different ways in SOA discussions. In this discussion of service design we consider the granularity of the services themselves, that is, the number of operations a service should have.

There can be no simple heuristic for determining service granularity. Rather, we offer two examples of factors that you should consider when designing services:

  • Services will usually be the unit of testing and release. If granularity is too coarse, if we group together a large numbers of operations in a single service, then we will tend to increase the number of consumers for the service. Hence if we make amendments to some aspect of a service, perhaps for the benefit of only a subset of consumers, we must re-release the whole service and hence potentially impact all consumers.
  • One challenge for the service consumer is to find the correct operations. Typically the consumer needs to browse a list of services and, having identified a suitable service, the list of service operations. We suggest that extremes in service granularity -- either many services with few methods, or few services with many tens or hundreds of operations -- will tend to impede consumability.

This indicates that in selecting service granularities we are likely to be trading off factors such as maintainability, operability and consumability. Any given SOA should provide guidance to service designers as they consider such trade offs.

Services are cohesive and complete

Having established that we need to be thoughtful when determining service granularity, what considerations apply in deciding which operations should constitute a service? We suggest that two object design concepts are useful: cohesion and completeness. We apply these concepts to the service interface.

We seek to create interfaces that are functionally cohesive, a set of operations that belong together because of their function. We find that when assessing the degree of cohesion, it is useful to think of the service from the perspective of the service consumer. By taking the consumer's perspective, we focus on the function of the service. Contrast this approach with using the following coherence criteria:

  • We could consider basing our decisions on the cohesiveness of the implementations of the functions. Should we group operations together because they use the same algorithm, or because they are implemented by exploiting CICS transactions on the same host? These are details of implementation that should not affect our interface design.
  • We could use a principle of temporal cohesiveness, that is, group operations that are used together in short time periods, for example the operations RetrieveCustomerDetails, CheckCreditRating, CreateLoanFacility and TransferFunds might well be seen in succession in a banking business process. However that temporary cohesion does not imply that these operations should be offered by the same service, CheckCreditRating and TransferFunds lack functional coherence.

Our use of the noun-verb naming convention for services and operations tends to help us focus on the functional cohesiveness of the service interface. We can ask the question, "Is the verb something that the noun does?"

Our second object design concept is that of completeness. The issue of completeness is particularly relevant when creating a service for a known consumer. In this case we will naturally tend to focus on meeting the requirements of the consumer we know. It is important to keep in mind that services should be reusable, and hence we need to consider the likely needs of future consumers. To give a trivial example, suppose a service called CellPhone offered operations such as Create, Update, Query, Delete, and Deactivate. We can well imagine the need to reactivate a deactivated cell phone, and so we should decide whether we should also offer the symmetric Activate method.

We should apply completeness considerations with discretion. Without known consumer requirements, it may be difficult to provide correct functionality and there is a danger of expending development and testing effort to produce operations that will not be used.

Services encapsulate implementation details

Another object design principle, that of encapsulation, also applies to designing service interfaces. Our motivation in encapsulating details of service implementation -- the algorithms and resources used -- is to increase the decoupling between service consumer and provider and hence enable future flexibility.

Services accommodate multiple invocation patterns

Web service technologies, such as those offered by WebSphere®, enable a further level of encapsulation. A service consumer can use identical coding techniques to invoke Web services using a variety of different invocation patterns such as the following:

  • Traditional, synchronous invocation using SOAP over HTTP
  • Asynchronous, message-based invocation using SOAP over JMS
  • Local invocation using Java procedure calls

However, although the Web service infrastructure may encapsulate details of invocation and hence simplify code, the service design should be influenced by the style of invocation. Consider the case of local versus remote invocation. A service design such as the one shown in LISTING 3 appears to offer a valuable business function, however it is unsuitable for deployment in many SOA environments.


Listing 3. "Busy" service interface

LibraryCatalogService {
// search operations elided

String getBookTitle(String isbn)

String getBookAuthor(String isbn)

Date getBookPublicationDate(String isbn)

// further operations elided
}

The service interface shown in Listing 3 is likely to work well when it is invoked locally. However, if the service is provided remotely from the consumer, the service may perform badly in common usage scenarios. Consider for example using the service to retrieve data to populate a screen displaying the catalog entry for a book. It will be necessary to make separate remote calls to retrieve title, author, and publication date. There is likely to be a significant performance cost in making these calls. A remote service should offer coarser-grained operations retrieving all the information for a book in a single invocation.

This design principle for remotely invocable service is widely known; we emphasize it here to illustrate that encapsulated details of service invocation can have very significant impact on how we choose to design services. We contend that the choice between synchronous and asynchronous invocation can have similar impact on service interface design.

This introduces a significant point: When a service is designed, what determines the invocation style to be used? Should a service designer be permitted free choice between local and remote invocation, between synchronous and asynchronous invocation? We recommend that the SOA should be prescriptive in this area. We make this recommendation for two reasons. First, we want to increase consumability by ensuring consistency; when choreographing a process it is preferable that services have predictable characteristics. Second, we want to increase flexibility by decoupling consumer from provider. By encouraging remote invocation we can decouple location, platform, and programming language. By encouraging asynchronous invocation, we decouple availability characteristics of consumer and provider.

If an SOA is to be prescriptive should it state that all services should be designed to permit remote, asynchronous invocation? We recommend a finer-grained approach to this prescription. There is a range of possible service types from those offering significant business-related operations such as PlaceOrder to more technical operations such as CheckUserInRole. It is entirely reasonable that the SOA should make different prescriptions for these different categories of services. We would expect that business-related operations would more usually be invoked asynchronously, where technical operations might reasonably be invoked locally.

Services have stateless interfaces

We argued in Services are designed for reuse that we should design services to be scalable and ready for deployment into high-availability infrastructures. One corollary of this general principle is that services should not be stateful. That is, they should not rely upon long-lived relationships between consumer and provider, nor should an operation invocation implicitly rely on a previous invocation. To illustrate this point we'll use the following telephone conversation:


Listing 4. Stateful conversation

Q:What is Dave's account balance?

A: It's £320

Q:What's his credit limit?

A:It's £2,000


This example shows two stateful aspects of a conversation. The second question refers to the first by using the word "his." This an example of an operation depending upon conversational context. Now consider the answers. Note that there is no contextual information in the words of the response. The answer is only meaningful because the questioner knows the question that was asked. In this example the consumer is required to maintain conversational state so that the answer can be interpreted. Both of these stateful relationships, between successive invocations and between request and response are relevant to SOA service design.

First we consider the implications of an operation depending upon context established by previous operations. Imagine that this was an interaction with a call center. So long as the conversation took place with the same operator, then the conversation completes effectively. But suppose instead that call were to be interrupted, as follows:


Listing 5. Interrupted stateful conversation

Q:What is Dave's account balance?

Operator 1: It's £320

An interruption occurs, and the caller talks to a different operator.

Q:What's his credit limit?

Operator 2: Who?


The interruption causes a loss of context so that the second question is meaningless. In the realm of telephone conversations, we can compensate for the interruption by reestablishing context: "I was just asking about Dave's bank details, could you tell me his credit limit?" However stateful conversations tend to be more troublesome in the realm of scalable service invocations, where enabling a context to be re-established may either be technically infeasible or may impose excessive performance costs.

In general, there is tension between building a scalable and reliable infrastructure and allowing stateful interactions. It is technically feasible to create a SOA infrastructure that supports stateful service invocation. Possible techniques include the following:

  • Using http cookies to maintain session context
  • Using stateful session EJBs; the handle to the bean is passed in the SOAP header

However we must carefully consider the scalability and reliability aspects of the resulting infrastructure. Is affinity required? That is, must successive requests from the same consumer be delivered to the same instance of the provider? A requirement for affinity is one way in which statefulness conflicts with scalability and reliability. If the infrastructure is free to deliver requests to one of many provider instances, then load-balancing is simplified and the reliability requirements for individual provider instances can be relaxed.

If there is to be no affinity requirement, if the infrastructure is to be permitted to deliver successive requests from a consumer to different provider instances, then any session state must be available in all provider instances. Application server infrastructures provide session replication mechanisms. The mechanisms can be used to make the session state available, but there is a performance cost in using them. Further, our experience of Web development shows that without firm guidance developers will make profligate use of session state; unduly large HTTP sessions have been a common cause of poor performance. See "Performance Analysis for Java Web Sites" by Joines, Willenborg and Hygh, pp 59-60 Addison-Wesley ISBN 0201844540.

We strongly recommend that services should be design to avoid the need to maintain session context.

Now let us consider the other stateful aspect of the conversation, the relationship between request and response. If we were to design services in the style of the telephone conversation above, relying on conversational context to interpret the response "What is Dave's credit limit?" - "£320" - then again we constrain the SOA infrastructure.

The infrastructure must accommodate possibilities such as some consumers being unable to retain its conversational state across temporary outages.

We can avoid the need for conversational state by designing the service to include suitable correlation information in the response, such as the following:


Listing 6. Conversation including correlation information

Q: What is Dave's credit limit?

A: Dave's credit limit is £2000


The response both identifies the person and provides the specific data. When wrapping legacy systems the responsibility for the provision of such correlation information often lies with the adapter. An existing synchronous API quite reasonably may not provide correlation data. The inclusion of correlation information in responses is good practice for a number of reasons. First, it simplifies the construction of resilient scalable solutions, but it is also a valuable diagnostic aid and may be essential in situations where it has not been possible to deliver error responses to the original requester. Undelivered messages may be placed on error queues, interpretation of such messages requires contextual information.

In summary, careful service design can avoid the need for stateful conversations and hence simply the implementation of a scalable, reliable SOA infrastructure.

Services are modeled using state-transactions

Moving on from our general advice to avoid reliance on the conversational state described in, we should remember that useful computer systems usually will be stateful; typically reflecting the life cycle of business objects.

For example, in the domain of shopping, consider the life cycle of an order: An order is created. From the user's point of view an empty shopping cart is created. The user then adds items to the order, and they are placed in the cart. Eventually the order is submitted and then the order is passed to the fulfillment department. Figure 1 shows this lifecycle modeled as a simplified state transition diagram.


Figure 1. Order life-cycle as state transitions
Order life cycle

The model makes clear some stateful behaviors. We see for example, that we can add line items to the order while it is in Open state, but not after it has been submitted.

Let us consider the design of an Order service. We might produce an interface of the sort shown in Listing 7


Listing 7. Order service design

OrderService {

void addLineItemToOrder(int orderId, int productId, int quantity)

void assignOrderToPacker(int packerId)

int createOrder(int customerId) // returns id of new order

int packItemForOrder(int orderId, int quantity) // returns quantity left to ship

boolean shipOrder(int orderId) // returns whether all order is now shipped

void submitOrder(int orderId)

// ... query operations elided ...


We want to consider the consumability of this interface. (To be more realistic, we should contemplate a full interface which has many more methods, such as those for listing and deleting line items.) Viewing even our small example without reference to the state diagram, it is quite difficult to discern the order in which methods should be invoked. So, we believe that a service designer must work to simplify the consumer's task. We offer a few possible techniques.

First consider the names of operations and parameters. The names in our example above are carefully chosen, and with some effort we can deduce the likely order in which the methods can be used. Compare the examples shown in Listing 8 and Listing 9, which are identical except for the names of the operations and parameters.


Listing 8. Poorly chosen operation and parameter names
 ZettuylService {
int wibble(int wibId, int wobId, String which);
int wobble(int quibId);
boolean wrubble(int wibId);
void quibble(int widId)
void quash(int wibId)
Stuff[] getStuff(int wibId );
void quite(int wibId);
Things[] getThings(int wibId);
void hinge(int wibId, intwobId);
int henge(int wibId , Stuff someStuff)
}


Listing 9. Well chosen operation and parameter names

ExpenseService {
int approveClaimItem(int claimId, int itemId, String comment);
int createClaim(String userId);
boolean auditClaim(int claimId);
void approveClaim(claimId)
void returnClaim(claimId)
ClaimItemDetails[] getClaimItems(int claimId );
void payClaim(int claimId);
ClaimErrors[] validateClaim(int claimId);
void removeClaimItem(int claimId, int itemId);
int addClaimItem(int claimId, ClaimItemDetails details)
}


The names in Listing 8 are all but unintelligible. The names chosen in Listing 9 clarify the purpose of the service, and it is possible to deduce much of the sequence in which operations should be invoked. For example createClaim() will be used before approveClaim(), which will be used before payClaim(). So as we stated above in Services are named to maximize consumabilit, the choice of names greatly affects consumability.

Next, remember how the state transition diagram for the Order clarified stateful behavior of the order. The diagram provides useful documentation showing the states of the order and which operations are appropriate in each state.

A second technique for increasing consumability is to remember the value of documentation, not all of which may best be delivered in the service interface definition. Well documented WSDL files are valuable but accompanying diagrams and examples also have considerable value.

A further technique for increasing consumability is to create service interfaces reflecting the states of the business object lifecycle. In our expense claim example each claim has a lifecycle of four states as shown Figure 1.


Figure 2. Four states of an expense object
Expense life-cycle

The states are significant for two reasons. First, each state tends to be relevant to different system users. For example, when the expense claim is in the building state, the primary system user is the claimant entering details of the expense claim, whereas in the auditing state, the claim is being inspected by a person with authority to approve the claim.

Second, transitions between major states commonly reflect the flow of data between different IT systems. For example, during the building state a thick client application on the user's workstation may be capturing the claim. When it is submitted, the claim is passed to a claim processing system, and when approved the claim may be passed to yet another system, a payment system. In passing, we would like to mention that if the implementation does indeed pass data from system to system then the operations responsible for the transitions between systems (in our example submitClaim() and approveClaim()) merit special attention. Their implementation will require updates to two systems and as such are vulnerable to loss of availability in either system. Implementations of these methods will tend to benefit from using asynchronous queuing mechanisms.

As the business object states tend to reflect both business and technical divisions, it may then be entirely reasonable to separate the original ExpenseClaimService into services applicable to each state. We would have services such as those shown in Listing 10.


Listing 10. Separating services according to state

ClaimEntryService {
createClaim(String userId);
ClaimItemDetails[] getClaimItems(int );.
ClaimErrors[] validateClaim(int claimId);
void removeClaimItem(int claimId, int itemId);
int addClaimItem(int claimId, ClaimItemDetails details)
int submitClaim(int claimId);
}

ClaimApprovalService {
int approveClaimItem(int claimId, int itemId, String comment);
void approveClaim(claimId)
void returnClaim(claimId)
ClaimItemDetails[] getClaimItems(int );.
ClaimErrors[] validateClaim(int claimId);
}

ClaimPaymentService {
void payClaim(int claimId);
}


Each service is then more easily understood. Further, this separation of interfaces is quite likely to fit well with how the service (or now the set of services) will be developed, deployed, maintained and consumed. The services are likely to be of interest to different consumers, they can be developed by separate development teams, deployed separately and hence have decoupled release-cycles. In other words, by focusing on the Object life-cycle we have established services with appropriate granularity.






Service Operation Design Principles

Having considered the overall design of services we can now move to consider the design of individual service operations.

Operations represent business actions

We have already stated the general principle that we should prefer business domain names for services and operations, using verbs for operation names. As we consider operations, we take this recommendation a step further: We should define operations with specific business meanings rather than generic operations. For example, rather than a general UpdateCustomerDetails operation, we find it useful to create operations such as ChangeCustomerAddress, RecordCustomerMarriage, and AddAlternativeCustomerContactNumber. This approach has the following benefits:

  • The operations correspond to specific business scenarios. Such scenarios may involve more than simply updating a record in a database. For example, a change of address or marital status may require production of formal documentation, and the system will be required to record the details -- or scanned copies -- of the documents. It becomes much more difficult to implement such business scenarios using a nonspecific operation such as UpdateCustomerDetails.
  • The individual operation interfaces will be simple and easy to understand, hence increasing consumability.
  • The unit of update for each operation is clearly defined (in our examples, address, marital status, and phone number). In implementing systems with high concurrency requirements we can devise finer grained locking strategies based on the access requirements of the operations, hence reducing contention for resources.

Operations have coarse-grained parameters

We again come to the question of granularity when we consider operation parameters. Compare the two interfaces for the CreateNewCustomer operation shown in Listing 11 and Listing 12 .


Listing 11. CreateNewCustomer operation interface with fine-grained parameters

int CreateNewCustomer(String familyName,
String givenName,
String initials,
int age
String address1
String address2
String postcode
// ...
)

Listing 12. CreateNewCustomer operation interface with single, coarse-grained parameter

int CreateNewCustomer( CustomerDetails newDetails)

Listing 11 shows an operation with many fine-grained parameters. InListing 12 the operation takes a structured type as a single coarse-grained parameter. We recommend using coarse-grained parameters for two reasons. First, they offer an opportunity for creating flexible operations, enabling new versions of the operation to be provided without perturbing existing consumers. Second, an operation with a large number of similarly typed parameters is vulnerable to transposition errors when invoked from 3GL code. In contrast when data is placed in a structured type explicit methods such as setGivenName() and setInitials() are used, and we find this approach to be less error-prone.

Operations are designed for concurrency

A traditional, transactional programming model such as that supported by Entity Enterprise Java Beans (Entity Beans) enables a database update to be implemented so that the database locking style is as shown in Listing 13.


Listing 13. Transactional programming model

Begin Transaction

Retrieve data from database - locking record

Modify values

Update database record with modified values

Commit Transaction - unlocking record


Note that database locks are held from the retrieval in line 2 until the commit in line 5. This ensures correct concurrent behavior at the expense of some latency. If we wish to design a service offering database update capability, we could offer operations corresponding to the retrieve and write operations in lines 2 and 4 in Listing 8. However, we strongly recommend against holding database locks between successive invocations in the highly decoupled, possibly asynchronous infrastructure of an SOA. Instead, we recommend the use of an optimistic locking strategy, delegating responsibility for concurrency control to appropriate application logic.

An update request in an optimistic locking strategy could be phrased "Here are some updates for record XYZ based on the version V of the record. Please make the changes only if no-one has modified the record since I read it."

Here is an sketch of an optimistic locking implementation involving the use of database triggers and revision counters for the same model shown in Listing 12. The implementation requires the following steps:

  • We add an extra integer column to the table for which optimistic locking is required; this holds a revision counter.
  • We add a trigger to database so that every update to a record in the table increments the revision counter.
  • All retrieve operations return data items that include the revision counter.
  • All update operations must include the revision counter obtained from the retrieval.
  • The update operation implementation must make a qualified update to the database record such as, "update record ... where the the revision counter equals ..." This update will fail if any intervening amendment to the record has occurred - when any intervening update occurred the update trigger would have fired and so the revision counter would have been modified.
  • If the update fails due to an intervening update by some other consumer, then a specific error is reported to the consumer.

Note how this implementation requires the consumer to provide an accurate revision counter when it is being updated; responsibility for correctness is spread across database, provider, and consumer. Also, note that this implementation is truly optimistic; it works well if the probability of contention is low. If update conflicts are likely, then the performance overhead of retries can be very costly. There are other possible optimistic locking strategies and detailed design work is needed to devise suitable concurrency schemes.

Given the relative complexity of managing concurrent updates, we make one related recommendation: Where possible use stateless semantics. For example, a single operation "Increment balance by X" can be implemented with good concurrent behavior much more easily than an equivalent "Retrieve record" - "Write record" pair of operations, with the consumer incrementing the value between the retrieve and write operations.





Conclusion

The primary purpose of this article is to emphasize the importance of service design in a Service-Oriented Architecture. We do not claim that the items we have discussed are an exhaustive set of design principles. Rather we hope that they will serve to illustrate the need for each SOA to thoughtfully identify an appropriate set of principles for its enterprise, and then to ensure that each service author applies those principles.