What exactly is service orientation, and what does it mean for the future of the software industry? What are the principles that should guide any developer using it? My recently published book, Programming WCF Services, is all about designing and developing service-oriented applications using WCF. But here I want to present my understanding of what service-orientation is all about, and what it means in practical terms.
To understand where the software industry is heading with service orientation, it helps to first appreciate where it came from, since almost nothing in this new methodology is entirely new but rather, the result of a gradual evolution of methodology and practice that has spanned decades. I'll begin with that story and after a brief discussion of the history of software engineering and the arc of its development, I'll describe what makes a service-oriented application (as opposed to mere service-oriented architecture), explain what services themselves are, and examine the benefits of using the service-oriented approach. I'll conclude by presenting several principles of service orientation and augment these abstract tenets with a few more practical and concrete requirements that must be met by most applications.
The first modern computer was an electromechanical, typewriter-size device developed in Poland in the late 1920s for enciphering messages. The device was later sold to the German Commerce Ministry, and in the 1930s was adopted by the German military for enciphering all communication. Today we know it as the Enigma. Enigma used mechanical rotors for changing the route of electrical current flow from a key type to a light board with a different letter on it (the ciphered letter). Enigma was not a general-purpose computer: it could only do enciphering and deciphering (today we call it encryption and decryption). If the operator wanted to change the encryption algorithm, he had to change the mechanical structure of the machine by changing the rotors, their order, their initial positions, and the wired plugs that connected the keyboard to the light board. The "program" was therefore coupled in the extreme to the problem it was designed to solve (encryption), and to the mechanical design of the computer.
The late 1940s and the 1950s saw the introduction of the first general-purpose electronic computers for defense purposes. These machines could run code that addressed any problem, not just a single predetermined task. The downside was that the code executed on those computers was in a machine-specific "language" with the program coupled to the hardware itself. Code developed for one machine could not run on another. Initially this was not a cause for concern since there were only a handful of computers in the world anyway.
As machines proliferated, the emergence of assembly language in the early 1960s decoupled code from specific machines and enabled it to run on multiple machines. However, the code was now coupled to machine architecture. Code written for an 8-bit machine could not run on a 16-bit machine, let alone withstand differences in the registers or available memory and memory layout. As a result, the cost of owning and maintaining a program began to escalate. This coincided more or less with the widespread adoption of computers by the civilian and government sectors, where the more limited resources and budgets necessitated a better solution.
In the 1960s, higher-level languages such as COBOL and FORTRAN introduced the notion of a compiler. A developer could write in an abstraction of a machine program (the language), and the compiler would translate that into actual assembly code. Compilers for the first time decoupled the code from the hardware and its architecture. The problem with those first-generation higher-level languages was that the code they generated was unstructured, resulting in code that was coupled to its own structure, via the use of jump or go-to statements. Minute changes to the code structure had devastating effects throughout the program.
The 1970s saw the emergence of structured programming via languages such as C and Pascal, which decoupled the code from its internal layout and structure by using functions and structures. During this same period, developers and researchers began to examine software as an engineered entity. To drive down the cost of ownership, companies had to start thinking about reuse--how could a piece of code be written so that it could be reused in other contexts? With languages like C, the basic unit of reuse is the function, but the problem with function-based reuse is that the function is coupled to the data it manipulates, and if the data is global, a change to benefit one function in one reuse context damages another function used somewhere else.
The solution to these problems was object-orientation, which appeared in the 1980s with languages such as Smalltalk, and later, C++. With object-orientation, the functions and the data they manipulate are packaged together in an object. The functions (now called methods) encapsulate the logic, and the object encapsulates the data. Object-orientation enables domain modeling in the form of a class hierarchy. The mechanism of reuse is class-based, enabling both direct reuse and specialization via inheritance.
But object-orientation is not without its own acute problems. First, the generated application (or code artifact) is a single monolithic application. Languages like C++ have nothing to say about the binary representation of the generated code. Developers had to deploy huge code bases every time, even for minute changes. This had a detrimental effect on the development process, quality, time to market, and cost. While the basic unit of reuse was a class, it was a class in source format. Consequently, the application was coupled to the language used. You could not have a Smalltalk client consuming a C++ class or deriving from it. Moreover, it turned out that inheritance is a poor mechanism for reuse, often harboring more harm than good because the developer of the derived class needs to be intimately aware of the implementation of the base class, which introduces vertical coupling across the class hierarchy.
Object-orientation was also oblivious to real-life challenges, such as deployment and versioning. Serialization and persistence posed yet another set of problems. Most applications did not start by plucking objects out of thin air--they had some persistent state that needed to be hydrated into their objects, and yet there was no way of enforcing compatibility between the persisted state and the potentially new object code. If the objects were distributed across multiple processes or machines, there was no way of using raw C++ for the invocation, since C++ required direct memory reference and did not support distribution. Developers had to write host processes and use some remote call technology such as TCP sockets to remote the calls, but such invocations looked nothing like native C++ calls and did not benefit from it.
The solutions to the problems of object-orientation evolved over time, involving technologies such as the static library (.lib) and the dynamic library (.dll), culminating in 1994 with the release by Microsoft of Component Object Model (COM), the first component-oriented technology. Component-orientation provides interchangeable and interoperable binary components. With COM, the client and the server agree on a binary type system (such as IDL) and a way of representing the metadata inside the opaque binary components, instead of sharing source files. COM components are discovered and loaded at runtime, enabling scenarios such as the dropping of a control on a form and having that control automatically loaded at runtime on the client's machine. The client only programs against an abstraction of the service provided by the COM object--a contract called the interface. As long as the interface is immutable, the service is free to evolve at will. A proxy can implement the same interface and thus enable seamless remote calls by encapsulating the low-level mechanics of the remote call.
The availability of a common binary type system in COM enables cross-language interoperability, and so a Visual Basic client can consume a C++ COM component. The basic unit of reuse is the interface, not the component, and polymorphic implementations are interchangeable. Versioning is controlled by assigning a unique identifier for every interface, COM object, and type library.
While COM was a fundamental breakthrough in modern software engineering, most developers found it unpalatable. COM was unnecessarily ugly because it was bolted on top of an operating system that was unaware of it, and the languages used for writing COM components (such as C++ and Visual Basic) were at best object-oriented but not component-oriented. This greatly complicated the programming model, requiring frameworks such as ATL to bridge the two worlds.
Recognizing these limitations, Microsoft released .NET 1.0 in 2002. .NET is (in the abstract) nothing more than cleaned-up COM, C++, and Windows, all working seamlessly together under a single, new component-oriented runtime. .NET supports all the advantages of COM, and mandates and standardizes many ingredients such as type metadata sharing, serialization, and versioning. While .NET is at least an order of magnitude easier to work with than COM, both COM and .NET suffer from a similar set of problems:
Technology and platform
The application and its code are coupled to the technology and the platform. Both COM and .NET are only available on Windows. Both COM and .NET expect the client and the service to be either COM- or .NET-based, and cannot interoperate natively with other technologies, whether they're on Windows or not. While bridging technologies such as web services make interoperability possible, they force the developers to let go of almost all of the benefits of working with the native framework and introduces their own complexities.
When shipping a component, a vendor cannot assume it will not be accessed by multiple threads concurrently by its clients. It fact, the only safe assumption the vendor can make is that the component will be accessed by multiple threads. As a result, the components must be thread-safe and equipped with a synchronization lock. If an application developer is building an application by aggregating multiple components from multiple vendors, the introduction of multiple locks renders the application deadlock-prone. Avoiding the deadlock couples the application and the components.
If the application wishes to have the components participate in a single transaction, it requires the application that hosts them to coordinate the transaction and flow the transaction from one component to the next, which is a serious programming fit. It also introduces coupling between the application and the components.
If components are deployed across process or machine boundaries, they are coupled to the details of the remote calls, the transport protocol used, and that protocol's implication on the programming model (such as reliability and security).
Components can be invoked synchronously or asynchronously, and can be connected or disconnected. A component may or may not be able to be invoked in either one of these modes, and the application must be aware of the exact preference.
Applications may be written against one version of a component and yet encounter another in production. Dealing robustly with versioning issues couples the application to the components it uses.
Components may need to authenticate and authorize their callers, and yet how would the component know which security authority it should use or which user is a member of which role? Not only that, but the component may want to ensure that the communication from its clients is secure, which of course imposes certain restrictions on the clients and in turn couples them to the security needs of the component.
Both COM and .NET tried to address some (but not all) of these challenges using technologies such as COM+ and Enterprise Services, respectively (similarly, Java introduced J2EE), but in reality, such applications were inundated with plumbing. In a decent-size application, the bulk of the effort, development, and debugging time is spent on addressing such plumbing issues, as opposed to business logic and features. To make things even worse, since the end customer (or the development manager) rarely cares about plumbing (as opposed to features), the developers are not given adequate time to develop robust plumbing. Instead, most handcrafted plumbing solutions are proprietary (which hinders reuse, migration, and hiring), and low quality, because most developers are not security or synchronization experts and were never given the time and resources to develop the plumbing properly.
As you consider the brief history of software engineering I've just outlined, you'll see a pattern: each new generation of technology incorporates the benefits, and improves on the deficiencies, of the technology that preceded it. However, every generation also introduces its own challenges, and I would say that modern software engineering is the ongoing refinement of the ever-increasing degrees of decoupling. Yet, while the history of software shows that coupling is bad, it also suggests that coupling is unavoidable. An absolutely decoupled application is useless because it adds no value. Developers can only add value by coupling things together. The very act of writing code is coupling one thing to another. The real question is how to wisely choose what to be coupled to.
I believe there are two types of coupling. Good coupling is business-level coupling. Developers add value by implementing a system use case or a feature, by coupling software functionality together. Bad coupling is anything to do with writing plumbing. What is wrong with .NET and COM is not the concept, but the fact that developers still have to write so much plumbing.
Recognizing the problems of the past, the service-oriented methodology has emerged in the 2000s as the answer to the shortcomings of object- and component-orientated methodologies, which I'll discuss in greater detail in the next section. For Microsoft developers, the methodology is embodied in the Windows Communication Foundation (WCF), which was released in November 2006 with the release of the .NET Framework 3.0.
A service-oriented application is simply an aggregation of services into a single logical, cohesive application (see Figure 1), much as an object-oriented application is the aggregation of objects.
Figure 1. A service-oriented application
The application itself may expose the aggregate as a new service, much like an object can be composed of smaller objects.
Inside services, developers still use specific programming languages, versions, technologies and frameworks, operating systems, APIs, and so on. However, between services you use standard messages and protocols, contracts, and metadata exchange.
The various services in a service-oriented application can be all in the same location or distributed across an intranet or the Internet, or they can come from multiple vendors, developed across a range of platforms and technologies, versioned independently, and even executed on different timelines. All of those aspects of plumbing are hidden from the clients in an application that interacts with the services. The clients send the standard messages to the services, and the plumbing at both ends marshals away the differences between the clients and the services by converting the messages to and from the neutral wire representation.
In a service-oriented application, developers focus on writing business logic, and expose that logic via interchangeable, interoperable service endpoints. Clients consume those endpoints, not the service code or its packaging. The interaction between the clients and the service endpoint is based on a standard message exchange, and the service publishes some kind of standard metadata, describing what exactly it can do and how clients should invoke operations on it. The metadata is the service equivalent of the C++ header file, the COM type library, or the .NET assembly metadata. The service's endpoint is reusable by any client that is compatible with its interaction constraints (such as synchronous, transacted, and secure communication), regardless of the client's implementation technology.
In many respects, a service is the natural evolution of the component, just as the component was the natural evolution of the object. Service-orientation is, to the best of our knowledge as an industry, the correct way to build maintainable, robust, and secure applications.
When developing a service-oriented application, you decouple the service code from the technology and platform used by the client; from many of the concurrency management issues; from the transaction propagation and management; and from the communication reliability, protocols, and patterns. By and large, securing the transfer of the message itself from the client to the service is also outside the scope of the service, and so is authenticating the caller. The service may still do its own local authorization as is dictated by the requirements. Much the same way, the client is agnostic of the version of the service: as long as the endpoint supports the contract the client expects, the client does not care about the version of the service. There are also tolerances built into the standards to deal with versioning of the data passed between the client and the service.
Since service-oriented frameworks provide off-the-shelf plumbing for connecting services together, the more granular the services are, the more use the application makes of this infrastructure, and the less plumbing the developers have to write. Taken to the extreme, every class and primitive should be a service to maximize the use of the ready-made connectivity and to avoid handcrafting plumbing. This, in theory, would enable effortlessly transactional integers, secure strings, and reliable classes. In practice, however, there is a limit to the granularity that is dictated mostly by the performance of the framework used (such as WCF). I do believe that as time goes by and service-oriented technologies evolve, the industry will see the service boundary pushed further and further inward, making services more and more granular, until the very primitive building blocks will be services. Evidently, this has historically been the trend of trading performance for productivity via methodology and abstraction.
Because the interaction between the client and the service in a service-oriented application is based on industry standards that prescribe how to secure the call, how to flow transactions, how to manage reliability, and so on, it is possible to create an off-the-shelf implementation of such plumbing. This in turn yields a maintainable application because the application is decoupled on the correct aspects. As the plumbing evolves, the application remains unaffected. A service-oriented application is robust because the developers can use available, proven, and tested plumbing, and the developers are more productive because they get to spend more of the cycle time on the features rather than the plumbing. This is the true value proposition of service-orientation: enabling developers to extract the plumbing out of their code and invest more in the business logic and the required features.
The many other hailed benefits, such as cross-technology interoperability, are merely a manifestation of its core benefit. You can certainly interoperate without resorting to services, as was the practice prior to its arrival on the scene, but ready-made plumbing provides the interoperability for you. When you write a service, you usually do not care which platform the client executes on--that is immaterial, which is the whole point of seamless interoperability. But a service-oriented application caters to much more than interoperability. It enables developers to cross boundaries. One type of a boundary is technology and platform, and crossing it is what interoperability is all about. But other boundaries may exist between the client and the service, such as security and trust boundaries, geographical boundaries, organizational boundaries, timeline boundaries, transaction boundaries, and even business model boundaries. Seamlessly crossing each of these boundaries is possible because of the standard message-based interaction. For example, there are standards for how to secure messages and establish a secure interaction between the client and the service, even though both may reside in domains (or sites) that have no direct trust relationship. There is a standard that enables the transaction manager on the client side to flow the transaction to the transaction manager on the service side, and have the service participate in that transaction, even though the two transaction managers never enlist in each other's transactions directly.
The service-oriented methodology governs what happens in the space between services. There is a small set of principles and best practices for building service-oriented applications referred to as the tenets of service-oriented architecture.
Service boundaries are explicit
A service is always confined behind boundaries such as technology and location. The service should not make the nature of these boundaries known to its clients by exposing contracts and data types that betray its technology or location. Adhering to this tenet will make aspects such as location and technology irrelevant. A different way of thinking about this tenet is that the more the client knows about the implementation of the service, the more the client is coupled to the service. To minimize the potential for coupling, the service has to explicitly expose functionality, and only operations (or data contracts) that are explicitly exposed will be shared with the client. Everything else is encapsulated. Service-oriented technologies should adopt an "opt-out by default" programming model, and expose only those things explicitly opted-in.
Services are autonomous
A service should need nothing from its clients or other services. The service should be operated and versioned independently from the clients. This will enable the service to evolve separately from the client. The service is also secured independently, and it protects itself and the messages sent to it regardless of the degree to which the client uses security. Doing so (besides being just common sense) also decouples the client and the service security-wise.
Services share operational contracts and data schema, not type- and technology-specific metadata
What the service does decide to expose across its boundary should be technology-neutral. The service must be able to convert its native data types to and from some neutral representation, and does not share indigenous, technology-specific things such as its assembly version number or its type. In addition, the service should not let its client know about local implementation details such as its instance management mode or its concurrency management mode. The service should only expose logical operations. How the service goes about implementing these operations and how it behaves should not be disclosed to the client.
Services are compatible based on policy
The service should publish a policy indicating what it can do and how clients can interact with it. Any access constraints expressed in the policy (such as reliable communication) should be separate from the service implementation details. Not all clients can interact with all services. It is perfectly valid to have an incompatibility that prevents a particular client from consuming the service. The published policy should be the only way that clients decide if they can interact with the service, and there should not be any out-of-band mechanism by which the clients make such a decision. Put differently, the service must be able to express, in a standard representation of policy, what it does and how clients should communicate with it. Being unable to express such a policy indicates a poor design of the service. Not that the service may not actually publish any such policy due to privacy (if it is not a public service). The tenet implies that the service should be able to publish a policy if it needs to.
The tenets just listed are very abstract, and supporting them is largely a facet of the technology used to develop and consume the services and the design of the service. Consequently, applications may have various degrees of compliance with the tenets, much the same way as developers can write non-object-oriented code in C++. However, well-designed applications try to maximize adherence to the tenets. I therefore supplement the tenets with a set of more down-to-earth practical principles:
Services are secure
A service and its clients must use secure communication. At the very least, the transfer of the message from the client to the service must be secured, and the clients must have a way of authenticating the service. The clients may also provide their credentials in the message so that the service can authenticate and authorize them.
Services leave the system in a consistent state
Conditions such as partially succeeding in executing the client's request are forbidden. All resources the service accesses must be consistent after the client's call. A service must not have any leftovers as a result of an error, such as only partially affecting the system state. The service should not require the help of its clients to recover the system back to a consistent state after an error.
Services are thread-safe
The service must be designed so that it can sustain concurrent access from multiple clients. The service should also be able to handle causality or logical thread reentrancy.
Services are reliable
If the client calls a service, the client will always know in a deterministic manner if the message was received by the service. The messages should also be processed in the order they were sent, not in the order they were received.
Services are robust
The service isolates its faults, preventing them from taking it, or other services, down. The service should not require the clients to alter their behavior according to the type of error the service encountered. This helps to decouple the clients from the service on the error-handling dimension.
While I view the practical principles as mandatory, there is also a set of optional principles that may not be required by all applications, although adhering to them is usually a good idea:
Services are interoperable
The service should be designed so that it can be called by any client, regardless of its technology.
Services are scale-invariant
They should use the same service code regardless of the number of clients and the load on the service. This will grossly simplify the cost of ownership of the service as the system grows, and allow different deployment scenarios.
Services are available
The service should always be able to accept the client's requests, and the service should have no downtime. Otherwise, if the service is unavailable, the client needs to accommodate for that, which in turn introduces coupling.
Services are responsive
The client should not wait long for the service to start processing its request. Having a nonresponsive service means the client needs to accommodate for that, which in turn introduces coupling.
Services are disciplined
The service execution of any operation is relatively short and does not take long to process the client's request. Having long processing means the client needs to accommodate for that, which in turn introduces coupling.
Juval Löwy is a seasoned software architect and the principal of IDesign, a consulting and training company that focuses on component-oriented design using Microsoft COM+ and the .NET platforms.
Return to the Windows DevCenter.
Copyright © 2009 O'Reilly Media, Inc.