Practical .NET

Architecting Services for Flexibility and Growth

The simplest way to ensure that the API your services expose to your consumers is to not let consumers access your services -- at least, not directly.

The purpose of design patterns is to empower developers. This purpose is woven into the heritage of design patterns: The original idea for software design patterns came from a book on architecture whose goal was to empower ordinary people to design and build their own homes. In the book, for example, there's a case study of a homeowner using the patterns to designg a home by integrating the appropriate patterns, tweaking each pattern to work with the other patterns, the site, and the people who will live in it.

If you want your services to form an architecture -- to be something other than a random collection of Web-based APIs with overlapping and confusing functionality -- then you're going to want to do the same thing with your service-oriented design. At the very least, you need your architecture to form a stable front end that makes it easy for developers to find and access the services they need to build their applications. No one wants a house where the front door keeps moving and where the path from the front door to the bathroom and on to the living room keeps changing.

The keyword in that paragraph is "stable", a goal that's difficult to achieve because, as your organization changes, as consumers needs change, and as the world changes, your services will have to change. It would be foolish to believe your initial set of services is the perfect implementation of everything your organization wants to offer. Despite that change, developers, having learned your API, don't want to have to continue to re-learn it as your services evolve. And they really, really don't want to have redesign and redeploy existing consumers because of changes in the messages used to make requests to your services. Service design patterns empower the service's consumers, not the service's providers.

Managing Services and Buses
One solution to this problem is to avoid having developers access your services directly. Instead, you provide a single virtual address that all consumers send all requests to (that virtual address may, of course, be an entrance point to multiple servers or some expandable cloud-based environment). This kind of implementation is an example of the Service Bus pattern: Consumers send requests to the bus and the bus distributes the requests to the appropriate services.

It would be an unusual organization that has a single service bus, however, and organizing your buses is going to be a critical task. It's tempting, for example, to create a service bus for each department in the organization (for example, an Accounting bus, an Inventory bus and so on) but that's the wrong approach to take. Most business operations cross departmental boundaries, and organizing buses around departments will make your consumers' lives harder because it forces consumers to coordinate activities across buses. Organizing buses by department is the functional equivalent of having a problem with a company, going to the company to get a solution and spending your day being shunted from one department to another.

Your service buses should be organized around functions that consumers require from your organization: placing orders, requesting actions, managing data -- regardless of which department owns the individual services involved. To put it another way: The service-oriented version of the Dependency Inversion principle should state that "Interfaces belong to the consumers, not the organization providing the services."

Buses also simplify access to services because, following the Single Responsibility Principle (SRP) you'll have many, many services. Just as in designing objects, the principle states "A service should do one thing well." Following the SRP enables each service to be the responsibility of a specific department, which simplifies management, funding and oversight (a service-oriented "ownership principle" would be "A service owned by multiple departments doesn't have any real owner at all."). You'll a have rich service ecosystem but, by organizing services around the functionality required by consumers, buses hide the resulting complexity.

Two other points: First, buses may not be the only entry point to services. To enable consumers to perform activities that aren't supported by the buses, some consumers may be allowed to access some of the services without going through the bus … recognizing that, as those services evolve, consumers won't be protected from those evolutionary changes. In many ways buses function as an implementation of what object-oriented developers call the Façade pattern.

Second, because departmental services frequently participate in multiple business processes, while each service is owned by a single department, each service may be accessible through several service buses.

Service Buses: Simple to Complex
In the simplest implementation of a service bus, requests come in and are distributed to every service. It's the responsibility of each service to accept only those requests relevant to the service. The first iteration of this design allows for multiple services to respond to a single request, by implementing the Aggregator pattern that assembles multiple response messages into a single response message. Effectively, this is the most simple-minded way to implement content-based routings: Messages aren't actually routed, but are instead accepted by the services that can respond to them.

In this design, a bus may initially submit incoming messages only to a single service -- the SalesOrder service, for example, which just sends back a copy of the sales order. Later, another service (the Shipping service, for example) may be added to the bus. Now consumers submitting requests to the bus will not only get a copy of the sales order but also information about when the order will arrive, aggregated into the response.

This design implements the Filter pattern for the individual services. As its name suggests, the Filter for a service is responsible for accepting or rejecting the messages that a service is designed to process. Filters accept or reject messages based on the message's content or its attributes (origin, transport mechanism, time received and so on).

However, a Filter can also be applied to the service bus as a whole. In this role, it would be better to think of the Filter pattern as being more like a signal processor. Like a signal processor, a service bus Filter modifies messages as they come in. Modifications include Enrichment (adding additional information or attributes) and Transformation (converting messages from one format to another). This process allows new services to depend on new message formats without disturbing old consumers by converting messages designed for the older services into the formats required by the new services.

Routing
Two problems become apparent as the number of services and requests increases, however. First, it becomes unwieldy for every service to get every message: Most services will reject the vast majority of the requests they receive. More critically, the order that the services are called in now becomes important: It's no longer reasonable for all the services that are interested in a message to process the message in parallel.

When adding a SalesOrder, for example, it may be necessary for a service that validates and creates the order to execute before any other services are invoked. The Router pattern makes the bus more sophisticated by taking messages (after they've been filtered) and sending them only to the relevant services on the bus while, potentially, also imposing a workflow on the processing. Routing rules will also include determining which Filters a message will be passed through on its way to the service(s) that process it or direct messages to other service buses in a "fire-and-forget" mode where eventual consistency is acceptable.

Initially, routing rules that control how messages are processed may be hard-coded into the service bus's router. In the long run, however, the bus's router will need to be built so the routing rules can be dynamically reconfigured as services are added removed, or modified. Ultimately, the bus may implement runtime routing: As a message comes in, a filter will dynamically determine the routing for the message and pass those rules to the router as one of the message's attributes.

This is all starting to sound very complicated. But it's important to note that things haven't become more complicated for the consumer. Through this process, the consumer may continue to send the same message to the same endpoint -- it's just that the response has become richer. Fundamentally, the consumer doesn't care about which service (or services) process the message. The consumer prepares a request that, essentially, is made up of hints that the router will use to determine which services should process the message. At this point, you've reached the final stage of Service Integration Maturity Model: you have an architecture of dynamically reconfigurable services that responds to individual requests.

You shouldn't, however, assume that you'll build all this infrastructure yourself. The complete system, as described here, includes the kind of functionality incorporated in Enterprise Service Buses (ESBs). It may make much more sense to purchase this functionality, customize it to work with your organization, and enable the functionality as you need it. If you've empowered yourself to build a house, you don't necessarily want to mill your own lumber or fire your own bricks, after all. The goal here is to empower the people living in the house (the consumers) not the people building it (that would be you). Sorry about that.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube