Chapter 8. Service antipatterns
In this chapter
- Pitfalls when moving to SOA
- Refactoring the knot
- Granularity of services
We’ve spent several chapters looking at SOA patterns. Antipatterns are the other side of the equation—instead of contexts and solutions, this chapter discusses common pitfalls you’re likely to stumble upon and how to avoid or refactor them. This complementary view is important, because it’s easy to make these mistakes when you’re starting out with SOA, even if you follow guidance such as the patterns we’ve already looked at.
Antipatterns, like patterns, are about contextual wisdom. A discussion of anti-patterns needs to talk both about when a behavior is a problem and when that behavior might be acceptable. The following sections will introduce each antipattern and then focus on the following topics:
In terms of granularity, services are sized so that a business process requires several of them to work together, but they aren’t small enough that they are end-nodes in the process, with other services only calling the service to obtain a result. This isn’t a bad thing in itself; after all, if each process were implemented by a single service, you’d have silos not unlike the ones you’re trying to escape by using SOA, and if you made the services too small, you’d fall into another trap (see the Nanoservice antipattern later in this chapter). The bottom line is that while the granularity is a force that drives us toward the Knot, there’s not a lot we can do about it without getting ourselves into worse problems.
Still, a better way to solve the service-to-service integration problem is to use an external orchestration engine. The idea of using the Orchestration pattern is to enable business process management—a way for business analysts and IT to control and verify that the processes are carried out as intended (you don’t have to use an orchestration engine for that, but it helps). In the context of solving or avoiding the Knot antipattern, the Orchestration pattern is better than the Workflodize pattern because it centralizes and externalizes all the interactions between services, effectively removing all the problematic code from the services themselves.
Getting the granularity of services right is one of the toughest tasks involved in designing services. There’s a lot to balance: the communications overhead, the flexibility of the system, the reuse potential, and so on. I can’t give you an exact recipe to follow to get service granularity right, because what is “right” depends on the context, the environment, and other decisions the service designers take. It’s easier to define what shouldn’t be a service than what should. For instance, you should definitely not call all of your existing ERP system a single service. The Nanoservice antipattern talks about the other extreme—the smaller services.
Unfortunately it isn’t always possible to find another suitable services (nano- or right-sized) that can assimilate the functionality of a nanoservice. In those cases, getting rid of a nanoservice is more of an exercise in redesign than it is a refactoring. I worked on a project that had a services allocation service (SAS). The SAS’s role was to know about other services’ locations, health statuses, and utilization, and upon request to decide what service instances should be used (such as at the beginning of a saga—see chapter 5 for a discussion of the Saga pattern). The service also provided reporting capabilities for active sagas, service utilization, and so on. This might not sound like a nanoservice, and at first we thought it wasn’t, but as the project progressed, we found that because the SAS was a central hub, as shown in figure 8.6, the SAS became a performance bottleneck. It incurred additional costs (in latency) on a lot of the calls and interactions made by other services. The utility of the SAS was being diminished by the cost. It was a nanoservice after all.
The situation is much worse in an SOA solution because each service can and will evolve independently, both in terms of deployment and functionality. What will happen when the Inventory service moves to another data center (for example, when it’s ported to the cloud)? What if the designers of the Inventory service decide that when the inventory level hits a threshold, the service will automatically order new supplies in a transaction? Now you can’t secure an item in the inventory until new supplies are ordered. All of a sudden, your transaction has expanded and now includes the Ordering service, the Inventory service, and a supplier’s service. As you can see, one risk of Transactional Integration is that designers of services participating in your transaction will extend the transaction to handle business rules they need to comply with.
Another alternative is to use sagas (see the Saga pattern in chapter 5). Sagas are basically long-running interactions (where messages are related and belong to the same conversation), but they don’t hold the same transactional guarantees as ACID transactions. In the case of an inventory problem, the ordering service will have to perform a compensating action to handle the problem. In order for this to work in a reasonable manner, the services may need to hold some data about the world, such as some data about inventory levels, so it can make a reasonable decision on its own.
The SOA tax refers to the fact that you have to invest more in both design time and runtime. SOA involves increased latency, for example, because there are additional layers like serialization and deserialization, communications, and so on. If instead of two services you could manage using objects that would talk to each other in the same memory space, you’d have none of that overhead. The SOA tax can also refer to the increase in local complexity of each component. The implementation for the data service as discussed previously was something like figure 8.9. You have a service hosted in a web server sporting a rich REST API that enables queries and the other CRUD operations, instead of just having the data access layer you’d have used otherwise. This service would also involve extra effort in testing, deploying, and monitoring.
The main trick with refactoring this antipattern is noticing it in the first place and acknowledging that you’re forcing whatever you used to do into SOA clothing. To help identify the problem, you can think about the fallacies of distributed computing (see section 1.1.3 of chapter 1). If you find that what you call “SOA” assumes one or more of them, that’s a smell that what you have might not really be SOA.
This chapter introduced some of the common pitfalls you’re likely to make when moving to SOA.
I mentioned at the beginning of this chapter that this second part of the book takes a look at different aspects of SOA in the real world. Now that we’ve finished looking at antipatterns, next we’ll look at another aspect of real-world SOA, which is that real problems are so big and complex that a single pattern can’t solve them. We’ll go over a case study of an end-to-end solution that integrates several patterns into a greater whole.
Leslie Lamport, Robert Shostak, and Marshall Pease, “The Byzantine Generals Problem,” ACM Transactions on Programming Languages and Systems, vol. 4, no. 3 (July 1982), http://research.microsoft.com/en-us/um/people/lamport/pubs/byz.pdf. This is a seminal paper that explains the challenge of distributed consensus.
Roger Sessions, “Shootout at the Transaction Corral; BTP versus WS-T,” ObjectWatch Newsletter, no. 41 (October 3, 2002), www.objectwatch.com/newsletters/issue_41.htm. This is a good paper by Roger Sessions from 2002(!) that explains why transactions between services are bad.