![]() |
Michael Gilfix Online |
Navigation |
Patterns for Proper Web Service Interface DesignUsing proper Web Service interface design can greatly improve application responsiveness and simplify integration logic. Poor design will not only bog down logic, but actually increase the complexity of both client and server-side implementation by requiring both to handle a larger number of possible error cases. This post attempts to capture three key patterns for designing/improving Web Service interfaces. While this article approaches the problem from the perspective of Web Services, many of these principles apply to Web Service design. Appropriate Granularity:The traditional interface design paradigms that folks are used to using in OOP and functional languages are often inappropriate for remote interfaces. They favor having many different methods that can be used to query information or state about an objects internals. This is particularly true for data objects. Since each query results in a remote interface invocation, querying information can quickly add up. A better approach is to use the Data Transfer Object (DTO) pattern that is heavily talked about in the concept of remote EJBs. The DTO is a data object that contains all queriable information in a single data object, which is transferred once. The cost of transferring more data in a single shot is marginal compared to the latency cost of going back and forth. This is particularly true for interfaces that allow you to create/update/query/delete data objects. As an example, consider a piece of data that identifies a customer. The customer has the following attributes: - ID: The customer identity. This uniquely identifies the customer. Consider a set of interactions that allow you to create, update, query, and delete a customer. Often times, a temptation is to design the interface as follows, trying to duplicate the smaller methods for a Java interface (Java syntax used below for convenience in expressing operation signatures): void createCustomer(String id, String name, String contact, String balance); void updateName(String id, String name); void updateBalance(String id, int balance); void setEnabled(String id, boolean status); String getName(String id); int getBalance(String id); boolean getEnabled(String id); void deleteCustomer(String id); Here's how that can be converted to using the DTO pattern and simplifying the interface using WSDL/schema. Consider a schema type for customer that looks as follows:
<xsd:complexType name="Customer">
<xsd:sequence>
<!-- ID is never optional -->
<xsd:element name="id" type="xsd:string" minOccurs="1" maxOccurs="1"/>
<!-- Name is optional -->
<xsd:element name="name" type="xsd:string" minOccurs="0" maxOccurs="1"/>
<!-- Balance is optional -->
<xsd:element name="balance" type="xsd:int" minOccurs="0" maxOccurs="1"/>
<!-- Enabled is optional -->
<xsd:element name="enabled" type="xsd:string" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complextType>
The ID is always needed to identify the customer, so it is non-optional in data object. We can now rewrite the operation signatures as follows with the implied contracts: // All optional fields are filled in during creation, unless they // are meant to be null on the data object void createCustomer(Customer data); // The non-optional ID uniquely identifies the customer. If any of the other // elements are present, then they are updated in the underlying object. void updateCustomer(Customer data); // Fetch all customer information Customer getCustomer(String id); // Deletes a customer void deleteCustomer(String id); This simplifies the number of operations that are needed to interact with a customer and minimizes the number of request response changes. This approach also allows the client to reuse object instances. The output of a get can be fed back into an update with a single field changed. The update is also flexible, providing the equivalent of updating multiple fields simultaneously when appropriate. While this step may seem somewhat obvious to some folks, it is an important predecessor to the next pattern. Do More at Once:It's better to batch multiple operations in a single call, rather than performing an invocation for each individual customer object. This amortizes the cost of the remote hop over multiple operations, improving throughput and responsiveness. First, we add an additional useful operation to our running customer example: the ability to validate customer information against what's stored in the underlying service. This operation looks as follows: boolean validateCustomer(Customer somebody); Typically these operation signatures would also have exceptions -or in WSDL parlance "faults" -that are raised in the advent of error while executing the operation. Let's assume each of the above methods throw an exception like this: boolean validateCustomer(Customer somebody) throws MyServiceException; To enable batching, we allow each operation to take in an array of customer data objects to operate on. But we can't leave our exception/fault scheme as-is. Raising a single fault from the Web Service invocation means that we have an ambiguous situation to resolve: what happens to the items that were updated while traversing the list when we stopped? Were they rolled back or persisted up to that point? And how do we identify where we succeeded? This can be resolved by introducing a new data object: <xsd:complexType name="FaultStatus"> <!-- A container for our service exception fault --> <xsd:element name="fault" type="somens:MyServiceException" minOccurs="0" maxOccurs="1"/> </xsd:complexType> Now we change our previous methods to return a list of fault status objects instead of raising a single fault exception. We need a container object for our fault so as to preserve the position of the result in the returned list. This pattern looks as follows for each of our method signatures: FaultStatus[] createCustomers(Customer[] data); FaultStatus[] updateCustomers(Customer[] data); Customer[] getCustomers(String[] id) throws MyServiceException; FaultStatus[] deleteCustomers(String[] id); Boolean[] validateCustomers(Customer[] somebodies) throws MyServiceException; Note that getCustomers() and validateCustomers still throw the single exception. This is because they don't update anything and thus do not suffer from the ambiguous situation. This is also what we want from the interface, which is to return the appropriate type. Reuse Data Object:Finally, where possible, reuse common data as sub-objects in larger objects. Ultimately this will improve cohesiveness of your interface, making it easier to supply information from one part to another. Schema makes these easy to do by embedding types as elements within larger types. Suppose in our example above that each balance had associated with it an account. Then we would have been better off using the following data model:
<xsd:complexType name="Balance">
<xsd:element name="account" type="xsd:string">
<xsd:element name="amount" type="xsd:int"/>
</xsd:complexType>
<xsd:complexType name="Customer">
<xsd:sequence>
<!-- ID is never optional -->
<xsd:element name="id" type="xsd:string" minOccurs="1" maxOccurs="1"/>
<!-- Name is optional -->
<xsd:element name="name" type="xsd:string" minOccurs="0" maxOccurs="1"/>
<!-- Enabled is optional -->
<xsd:element name="enabled" type="xsd:string" minOccurs="0" maxOccurs="1"/>
<!-- Balance is optional -->
<xsd:element name="balance" type="somens:Balance" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complextType>
Now consider we had an operation with the following operation signature that updates account balances: FaultStatus[] updateBalance(Balance[] balances); We could now call getCustomers() to get a list of customer information. We can then reuse the object instances in each customer's balance as input to the updateBalance() method, whereby we could tweak balances before updating. The client-side logic would then be simple: it would iterate through customers looking for balances to update and append updated balance objects to a list. This list would service as the input to the update balance operation. Wrapping it upUse of these three guidelines greatly simplifies Web Service interacts. The patterns in these guidelines can be repeatedly applied during interface design in order to achieve the minimum number of interactions and the best cohesiveness between different operations. A side effect of applying these patterns is that this will also simplify client-side (and even possibly server-side) code. A lot of error cases disappear when it is no longer possible to prove partial updates or provide recovery for each fine grained operation. By Michael Gilfix at 2007-02-26 02:16 | Design | SOA | Web Services | Michael Gilfix's blog | login or register to post comments
Depends on semanticsThere are different ways to approach atomicity. One is to have your batched updates occur in a single all or nothing transaction, or to allow whichever ones to succeed despite failure. This post describes the latter approach. This can often be useful for bulk operations, which you want most of the changes to apply despite a few failures. For example, if you are inserting duplicates. The system never goes down, you just try to process as many as possible. An example is when initializing a set of a user registry with a bunch of users, where 99 succeed and 1 fails. You don't want to roll the whole thing back. You can still achieve the single transaction atomicity by submitting one at time - there's definitely an obvious tradeoff there. |
SearchRecent blog posts
|
Atomic batch operations
As described in the post, no client of this interface can issue multiple CRUD operations within a single atomic unit of work. That includes even if the web service later adds WS-MAGICALTRANSACTIONALMYSTICALSTUFF semantics to the implementation. Quite a limitation.
Having an all-or-nothing atomic create/update/delete semantic is useful when modifying elements explicitly by ID. All-or-nothing meaning either the entire batch operation is well formed/succeeds/commits or a fault/exception occurs and the entire batch operation rolls back.
Atomic batched operations has two major benefits:
Clients wishing to perform many individual, atomic updates can just issue multiple web service operations.
Also wouldn't hurt to explain why many small operations is an antipattern...