Software Architecture: Service Oriented Architecture, Part 1
This post begins a discussion of enterprise software architecture. (Find another definition, here.) Because it's currently such a popular buzz phrase, we start with some thoughts on SOA (service oriented architecture).
Just to establish the playing field (or "level set", to use a popular buzz phrase), here are some characteristics I would ascribe to services:
- Services are exposed (though not necessarily "publicly") so that a wide variety of clients may access them. For many, this implies web services, but in some environments this may imply EJB or some other mechanism. This may also imply a non-RPC style message, though that is a slightly different discussion.
- Services are stateless. That is, the service does not "remember" the results of prior calls, and there is no order dependencies between various service methods.
- Services should be as simple as possible to use, and should not require the client (the thing calling the service) to do a lot of "work" to setup for a call, or necessarily upon return from a call. A dead giveaway that a service is poorly designed is the amount of client code required just to make some given function call (other than the "plumbing" code that may be required). One of the arguments used against services is the relatively large amount of plumbing code (AKA "glue") needed to support their use.
- A service should make few, if any, assumptions about its clients, nor can clients assume anything about the service (other than the interface contract). This is usually called "loose coupling". In particular, service clients should be ignorant of the service's implementation, and vice versa.
- Service interfaces should be as immutable as possible. Forever. I say "should be" because in the real world, sometimes things have to change. However, the cost of changing a service interface is very high.
- Services do not necessarily have to be transactional. If not, then each update function should have a compensating transaction ("undo" function).
The above list is by no means exhaustive, and I reserve the right to modify my definition at any time.
Transactions
Today we will talk about transactions, and why they might not be quite as good an idea as previously thought. First off, let's get a perspective from Sun, in their J2EE Blueprints, section 8.8:
"Most enterprise information systems support some form of transactions. For example, a typical JDBC database allows multiple SQL updates to be grouped in an atomic transaction.
Components should always access an enterprise information system under the scope of a transaction since this provides some guarantee on the integrity and consistency of the underlying data. Such systems can be accessed under a JTA transaction or a resource manager (RM) local transaction."
Unfortunately, this view is both naive and makes assumptions about the nature of the types of "systems" that will participate in any given sequence of service method calls. Specifically, systems (enterprise or otherwise) are not databases, and should never be viewed this way by clients. (This is a huge pet peeve of mine.) While we may design service methods to be transactional, I don't think anything should be assumed about the nature of the underlying data store(s), nor can we be tied to a particular kind of client environment, either. (Sun seems to favor the JEE client environment, for some reason.)
From another document on SOA, this quote from Microsoft's Principles of Service Design states:
"While services are designed to be autonomous, no service is an island. A SOA-based solution is fractal, consisting of a number of services configured for a specific solution. Thinking autonomously, one soon realizes there is no presiding authority within a service-oriented environment—the concept of an orchestration "conductor" is a faulty one (further implying that the concept of "roll-backs" across services is faulty—but this is a topic best left for another paper). The keys to realizing autonomous services are isolation and decoupling. Services are designed and deployed independently of one another and may only communicate using contract-driven messages and policies."
Requiring a transaction for every service method may seem like a good idea, but it can't always be done. Some system data stores may simply not be transactional. I can think of one in particular: LDAP. The best you could do would be to create a compensating transaction (and here I use the word "transaction" in the sense that a method call, being an interaction with the service, is considered a transaction with the service).
There can be some problems, however, with compensating transactions:
- Maintaining system integrity across multiple services can become very difficult, especially as the number of failure scenarios increases.
- If updates must be ordered (i.e., performed in a specific sequence), so must the undos, usually.
- Keeping both updates and "undos" idempotent can also be quite difficult. Particularly, the undo must not "undo" an update that was never applied!
- In some scenarios, concurrent reads by other clients will see the results of "partially committed" updates that may be subsequently undone. These are in essence uncommitted reads, which may be a problem. It most certainly can become a problem when "undoing" a transaction that was applied based on the results of a prior uncommitted read.
The above difficulties might lead one to devise "service coordinators" that exist for the sole purpose of ensuring the integrity of their underlying systems. That is, we aggregate our enterprise systems below the service boundary, exposing services as essentially autonomous (single unit of work) functions. One call does all, in other words. Clearly, for some situations this may be necessary. Hide all the complexity behind a nice, clean, service boundary.
One suggestion is to ensure that all participants are transactional except for one. The non-transactional update is performed last (after it is known that all the others have at least provisionally committed), so that the transaction would only need rolling back if the non-transactional update failed. If the transaction fails before executing the non-transactional component, then no compensating update is needed. This, though, seems counter to Sun's recommendation. You may also want to order updates with the most-likely-to-fail being performed first, so that the amount of work is minimized when an "expected" failure occurs. Of course, you don't always have this flexibility.
The order of updates may be important, but with compensating transactions, knowing exactly what needs to be undone can become impossibly complex, again, unless the number of non-transactional services (participating in an update function) is limited. I would recommend not having updates be dependent on the results from prior, uncommitted updates in the same transaction. These update transactions would not be idempotent, in any event.
Clearly, this issue (and many of the issues inherent to the design of services) is quite complex and there is no one, single, right answer. At no point along the way can judgment be eliminated in favor of a cut-and-dried cookie-cutter approach.
At some lower layer in the service stack transactions may be necessary, if not required. This may be the essence of the "Enterprise Service Bus" approach. At other, "higher" layers in our model, transactions may not either be practical or even possible.
In the next post we will talk about reliable messaging.


0 Comments:
Post a Comment
<< Home