Most of the technologies used for service-oriented architectures are fairly easy to implement. Web services, for example, uses XML and HTTP, both of which are fairly easy to understand and use. The problems start when we begin to design the interfaces for the services we want to create. Meeting the needs of all of the varied and numerous clients that use our services now and in the future is difficult.
One of the benefits of service-oriented architecture is service assembly. Developers assemble the services we create into working applications. Unfortunately, we don’t always know how our services will be used in these applications. It is especially difficult for us to predict how our services will be used in future applications. This is one of the greatest challenges for us as service designers. We usually make our best guess as to how the services will be used when we are determining the way to structure their interfaces. Because services are executed across a network, it is especially important that we get the interfaces right. If
we get them wrong, our clients will either receive more data than they need, or they will have to make multiple trips to the service to get all the data they need.
There are two ways to apply the concept of granularity to services. The service itself can be coarse grained or fine grained. This refers to how much functionality the service covers. Let’s assume that we need to create an application for manipulating a
bank account. Suppose the application needs to manipulate both a checking account and a savings account. We have two choices when we create a service to support this function. We can create a coarse-grained service called BankAccountService that manipulates both checking and savings accounts. Or, we could create two fine-grained services, a SavingsAccountService and a CheckingAccountService. Because BankAccountService supports the functionality of both checking accounts and savings accounts, it is a coarse-grained service.
Granularity also applies to the way that service methods are implemented. Suppose in our BankAccountService, we create a method called GetAccountHolder. A coarse-grained implementation of this function would return both the account holder’s name and address. A fine-grained version of this function would return just the account holder’s name. A separate method called GetAccountHoldersAddress would return the address. A service method that returns more data is a coarse-grained method. A service method that returns less, more specific, data is a fine-grained method.
Because services will be used in ways that we can’t fully anticipate when we design them, the decision about granularity does not have to be black and white. Services do not have to be coarse-grained or fine-grained; they can be coarse-grained and fine-grained, or Multi-Grained. In other words, the BankAccountService, the SavingsAccountService, and the CheckingAccountService can all exist together. If a client only needs to access a customer’s savings account, the client should use the SavingsAccountService. There is no need for the client to know anything about checking accounts. If, on the other hand, a client needs to know about both checking accounts and savings accounts, it should use the BankAccountService.
Why do we need to create the composite BankAccountService if we already have a SavingsAccountService and CheckingAccountService service? We should build services that are as easy as possible to use and meet the expectations of the clients that use them. It seems logical that a client would more often than not want access to both checking and savings accounts. By implementing both interfaces, we get the best of both worlds.
Initially creating fine-grained services and then wrapping them in coarse-grained facades accomplishes the creation of multi-grained services. For example, consider Diagram 1.
It is also possible to create fine-grained facades that access coarse-grained services. However, it is better to create finer-grained base services for reasons such as service deployment and management. Smaller services will provide more options for their physical deployment.
If we incorrectly predict the granularity of the service, clients will have access to more functionality than they absolutely need. This can be a problem if we have security at the service level. It might not be possible to restrict a client from some methods and not others, only to the entire service. If this is the case, we might have to open up the entire service to the client. This problem is avoided if we designed the service at the appropriate level of granularity.
The service interfaces constitue an established contract between the services and the clients. One of the tradeoffs when creating multiple interfaces is that each interface is essentially a published contract. Additional interfaces make managing these contracts between clients and services more difficult.
The granularity of the methods within a service is of equal or greater importance than the granularity of the service itself. Using our bank account example, consider the retrieval of account holder information from the bank account service. A method in the BankAccountService called GetAccountHolder could return just the name of the account holder, or both the name and address of the account holder. As shown in Diagram 2, the BankAccountService returns account holder information with both the name and address.
This scenario works great if the client needs the address information, but if the client doesn’t need address information, more data crosses the network than is absolutely necessary. By eliminating the address from the account holder information, we meet the needs of clients that don’t need address information. But if we eliminate the address, clients that need address information will have to return to the service to retrieve it. Diagram 3 shows this scenario:
In this scenario, a client that needs both name and address will have to make two network trips to retrieve the street address. To solve both of these problems, consider a BankAccountService with two methods, GetAccountHolder and GetAccountHolderAddress. The GetAccountHolder method returns just the account holder’s name. The GetAccountHolderAddress returns both the account holder’s name and the account holder’s address. An alternate implementation could involve passing in an argument that directs the service as to how much data to return. In a sophisticated implementation, a client would pass the service the list of attributes to return back to it. If these implementations are not possible, it is always better to return more data to minimize network round trips. It is likely that future clients will need the data provided by the service.
The ability of a service to have multi-grained methods that return the appropriate amount of data is important to reduce network traffic. Extra network traffic is either due to too much unnecessary data or too many requests to get the data that a client requests.
Granularity is a difficult problem for us to reconcile when we design service interfaces. It is important to understand the options and to implement the most appropriate interface. In the past, arguments surrounding service interfaces have focused mainly on determining the right granularity. With multi-grained services, it becomes important to find the right granularities for your clients.
About the Author
Michael Stevens is an independent consultant specializing in service-oriented architectures for the enterprise. He has over fourteen years of experience in information technology architecting and developing software systems, most recently focusing on J2EE solutions. He founded a software company that developed solutions for the mailing industry. He is a certified Java programmer, a member of the IEEE Computer Society, and of the ACM. He may be reached at [email protected].