Chapter 7. Service Engineering

It will not come as a surprise that there is a simple connection between the concepts of promises and services. Promises are valuable to other agents when kept reliably, or at last predictably, and they can even be sold or traded. This is the basis of a service.

Service buoys the human experience, on a day-to-day basis. There are dumb services, like web servers and directory service lookups, or ATM cash-point transactions, where we don’t expect or even want too much intelligence, but there are also experiential services like hotels and restaurants where humans are directly involved, and the subtle semantics are everything to the assessment of the outcome.

Services make up a potentially complex topic because they are a meeting place between the key ingredients of promises: semantics, dynamics, and valuation.

  • Services have to work in detail (semantics).

  • Services have to operate at some scale (dynamics).

  • Services have to be economically viable to survive (economics).

In this chapter, I want to sketch out a few of these issues. The real power of Promise Theory comes from a more formal, symbolic toolset, but that is the subject of a different book.

Promise Theory makes a simple prediction about services, which is possibly counterintuitive. It tells us that the responsibility for getting service ultimately lies with the client, not the server. That clarifies a few things about the design of systems where services play a role.

The Client-Server Model

The client-server model is one of the classic patterns in information technology, and business. A client is any agent that makes a request, and a server is any agent that responds to the request and performs the service. There are two ways to model this:

  • A client makes a spontaneous imposition upon the server (i.e., the client “throws a request over the wall,” and the server is expected to catch it). This is often the case for an irregular interaction, like buying something from a vending machine, or dropping in for an emergency room visit.

  • A client maintains a continued promise relationship, with more forethought to arrange mutual promise (e.g., a regular delivery of mail is promised at 10 each morning, and you promise to accept it). A normal doctor’s appointment, even though irregular, is usually promised in advance, because doctors do not promise to receive patients without an appointment. The patient is subordinate to the doctor’s availability.

If we string together many impositions, it makes sense to negotiate a more formal promise relationship. In other words, the more continuity of intent we have, the more promises rather than impositions make sense.

What promises add is the recognition of an ongoing relationship. An imposition is a fire and forget event that exists in the moment it is made, but a promise is something that is known for longer and the time (or times) at which it is kept is not tied to a single moment. Thus, when we think in promises, we plan for more of a relationship of continuous verification. This is what services are all about.

Responsibility for Service Delivery

The responsibility for obtaining lies with the client, because no service can be transmitted without the client accepting a service promise from a server. Even if there are multiple servers promising the service redundantly so that the possibility of service is guaranteed, the client needs to make a choice to use one or more of them. The client is the single point of failure (see Figure 7-1).

image
Figure 7-1. A client has to promise to use a service (-) in order to avail itself of the offer (+). No matter how many servers there are, failing or keeping their service promise, the client has to choose and accept a good one. So the client has ultimate responsibility for getting service.

Similarly, if there is no server offering the service and the client needs the service, it is up to the client to locate an agent promising the service and bind to it.

Redundancy, on the provider side, does not immunize against all possible failures; it only increases the likelihood of a user being able to keep a promise to use the service. This should tell us something important. The responsibility for obtaining a service lies with the intent to obtain service (i.e., with the user, not with the provider). A provider can exercise best-effort and no more.

Dispatchers and Queues for Service on Demand

In many services, clients arrive at some kind of queue or waiting area, imposing themselves as clients for service. The existence of the waiting area is effectively a promise to accept their impositions. Clients are assigned a server by an agent called a dispatcher, when one becomes available. For example, customers arrive without an appointment at a restaurant or at an emergency room.

In technology, dispatchers are also called load balancers, as tasks of some magnitude might be shared between multiple servers. For instance, a client might request taxi transport for eight people. A dispatcher might then share the load between two taxis (see Figure 7-2).

image
Figure 7-2. A dispatcher can act as a go-between service or proxy for finding a server for incoming clients, but the responsibility to use still lies with the client.

The dispatcher becomes a proxy or broker for the acquisition of a service provider. When many requests arrive, the dispatcher might need to share its load with another dispatcher, too. Quite soon, a hierarchy of service responsibility emerges. However, in all cases, the client has the responsibility of accepting an agent to interact with, so the role of the dispatcher could easily be eliminated if the agent had knowledge of the possible servers.

Delivering Service Through Intermediaries or Proxies

Let’s explore the idea of proxies in more depth. Suppose the giver of a promise requires the assistance of other intermediary agents in order to keep its promise. The basic experience of promise conditionals may be used to address this: a server can promise its service conditionally if the proxy promises to assist. Let’s examine how such an agent can promise delivery of goods and services through a number of intermediaries. This is the so-called end-to-end delivery problem, and it forms the basis of many similar patterns, including postal delivery agents, transportation agents, publishers, cabling infrastructure, waiters, actors, postal workers, contractors, and even buses.

Schematically, we typically imagine the problem as a story-line narrative, as in Figure 7-3.

image
Figure 7-3. Schematic service delivery through a proxy or intermediate agent.

This figure is a simplistic after-the-fact idealization of what actually happens during a single enactment of events to keep a promise once. It does not represent a state of process continuity. What happens if the item or service is not delivered? How does this distinguish discrete packages (like books) from continuous streams (like video)? Table 7-1 provides some examples of proxy delivery.

Table 7-1. Examples of proxy delivery
Server Proxy Client

Factory

Truck

Showroom

Player

Ball

Player

Music

Stereo system

Listener

Phone company

Phone

Phone client

Framing Promises as State or Action

Although I’ve deliberately focused on a singular view of promises, there are two ways we could formulate even a chain of promises: as an imperative sequence of relative changes, or as a declarative statement of a desired end state. In general, the latter is preferred for its robustness and consistency.

Consider the different formulations:

  • Promise to act (push delivery, make a relative change)

  • Promise the final outcome (continuous pull, assure outcome)

The first of these represents a discrete, one-time delivery of the outcome to the agent, which might be satisfied but is never repeated. Indeed, it cannot be, because it promises something relative to a state that also promises to destroy that state. There is also the possibility that the state changes before the promise can be kept, so the value of the imperative promise is somewhat in doubt.

For example, a slalom skier could make a series of promises to use the ski markers, or could simply promise to get to the bottom, without observing such constraints (Figure 7-4). These two are related by conditions. The push promises:

  1. I promise to go to post 1, then

  2. I promise to go to post 2.

image
Figure 7-4. Promising a sequence of steps, or a desired end state.

The conditional end state promises:

  1. I promise to be at post 1, and then this promise expires.

  2. I promise to be at post 2 if I have been at post 1.

Once the promise of a single event has been kept, it is usually kept just once. To repeat a promised event, or make a replacement, we would require a new and different promise altogether. Thus we can write promises in the first form, where they can also behave like prearranged impositions. This is called an imperative pattern. Alternatively we can write them in the second form, representing a continuous attempt to ensure that the agent always achieves the outcome. This is called a declarative or desired end-state pattern.

This second kind of statement can persist for an indefinite time, and the state of the agent can be maintained, even for future slalom races. Thus it documents more than a single event in time. This is important, because, in the world of services, time is not a very interesting variable. Services are usually delivered as steady-state processes (i.e., so-called continuous delivery).

Although we envisage agents in a continual state of readiness to keep promises, impositions may be quenched only once, since the states they depend on are lost in a timeline. Continuous promises kept on-demand last for extended times. We may use these extreme positions to model multiagent workflows that are either one-off triggered events or continuous flows.

Delivery Chains by Imposition

The delivery chain is an important abstraction that applies across a whole range of different industries. A lot of different service-oriented workflows can be cast in this image. As the length of the chain gets longer, the complexity of trust relationships becomes very complex, so I’ll restrict this discussion to a single intermediary agent or proxy, as shown in Figure 7-5. As usual, there are two approaches to delivery.

For blind trust, a widely used pattern for working is the “fire and forget” imposition pattern.1

image
Figure 7-5. Imposition-based workflow, or “fire and forget.”

Fire and forget is an event-driven view; the server tries to push a change or delivery by imposing it on the delivery agent. This is called fire and forget because the trigger event does not seek or obtain any assurance of final outcome. The intermediary or proxy promises to accept such impositions.

  • Server imposes “take this delivery” onto Proxy (-D1)

  • Proxy promises “accept delivery” to Server (-D1)

  • Proxy imposes “take this delivery” to Client (-D2)

  • Client promises “accept delivery” to Proxy (-D2)

We need a more mathematical notation to make these promises precise, but the basic intent should be clear from Figure 7-5.

This approach is based on ephemeral impositions, without any assessment. These are clearly fragile. What if the imposition is not accepted, or was dropped accidentally? If the chain fails somewhere, it would require a new imposition to restart the intended sequence of events. In the absence of a promise to achieve a desired outcome, there is no implication that the agents would try more than once to keep the promise. As a result, the server ends up with no assurances about what happens at the client end.

Delivery Chains with Promises

What if we use promises and desired end states instead of impositions? This implies a more long-standing relationship between the parties. Take a look at Figure 7-6.

image
Figure 7-6. Desired state, self-maintaining workflow, or assured delivery. Documenting all conditionals in promises is a more honest way of bookkeeping fragile dependencies.

The server promises the client end state, such as the delivery of some good or service: “You will have the package,” conditional on “if the delivery agent promises delivery.” The server also promises to use a promise to deliver the package from the proxy: “Deliver package to client.” Recall that the combination of these two promises may be written more simply, by the law of conditional assistance, as:

  • Server promises to “provide service if proxy promises delivery” to client

  • Server uses a “promise to deliver to client” from proxy

By the law of conditional promising, this may be considered a promise, albeit a conditional one, because the client has sufficient information that the server is covering its requirements:

  • Server promises “delivery conditionally on proxy” to client

We can now use this last item as a shorthand for the two preceding promises.

This all sounds like a simple thing, but it leads to a surprising amount of complexity in terms of promises. Remember that these promises have to follow the rules of binding and only promising an agent’s own behaviour. When we follow through this discipline, we end up with a plainly accurate version of documented cooperation, but at a level of detail that we might not have been expecting.

The full collaboration now takes the form:

  • Server promises “delivery conditionally if proxy delivers” (P if D) to client (+)

  • Server promises “to provide package” (+P) AND “to use delivery service” (-D) to proxy

  • Proxy promises “to deliver packages” (+D) AND to “accept packages” (-P) server

  • Proxy promises “package conditionally if server provides” (D if P) client (+)

  • Client promises to accept the terms “delivery conditionally if proxy delivers” (P if D) server (-)

  • Client promises to accept the terms “package conditionally if server provides” (D if P) proxy (-)

The bindings are now complete. We have a total of eight (shortened) promises for this simple exchange between three parties. It might be a surprise just how many promises we need to have explicit documentation or assurance of the behaviours of the agents. This is one of the aspects of thinking in promises that is most valuable. By following a straightforward, logical discipline, the complexity of cooperation unravels before our eyes, revealing all of the things that might go wrong.

Formality Helps the Medicine Go Down

As the length of cooperative chains increases beyond a single proxy or intermediary, it is difficult to represent all of these promise interactions without a more formal language. The full version of Promise Theory requires a mathematical formulation, but for now it suffices to add the labels (P,D) in the manifest, and we see how the circuitry of + and - promises connects.

The symmetries between + and - polarities in the promise collaboration, and between P and D, indicate the complementarity of the promises. The server promises its client, “I will give you P if the delivery agent promises you D.” The delivery agent says, “I will deliver D if I receive P from the server.” Both agencies are promising the client something that requires them to work together, and the only difference between them from the client’s viewpoint is the realization of how the promises are kept.

This promise from the server to the client represents a virtual interface between the two, which could not be represented at all in the push-imposition model. It represents the effective promise from the server to the client, and the client accepts.

  1. In two promises, the server promises to deliver the end state “package received” (P) via the proxy delivery agent.

  2. To accomplish this, the server promises to hand over the package (+P) to the proxy, and the proxy promises to accept such transactions (-P).

  3. The proxy promises the server that it can deliver (+D) to the client.

  4. The delivery agent promises to deliver what it received from the server (+D if P), because it needs confirmation (-D if P) from the client that it is OK to deliver, assuming that condition P was quenched by P being kept. Equivalently, it will deliver when the client makes its pull request to acquire the delivery.

This might all seem like a lot of fuss. Importantly, it reveals the intricacies we take for granted in making promises through an intermediary.

The neat thing about Promise Theory is that it turns these complicated considerations into a simple, symbolic algebra. If you are not mathematically inclined, think of it as a simple bookkeeping methodology.

Chains of Intermediaries

Suppose there is not just one intermediary, but a chain, like a game of Chinese whispers. This happens by recursion. The delivery agent (a contractor for the server) subcontracts the delivery to another company. Then we have to repeat the proxy relationship between the first proxy and the second proxy. But additionally, now the first proxy promise has to be conditional on the second, so the original server is informed of this subcontracting through the change of promise. Similarly, the additional promises to assure that the first proxy will indeed engage the second, the third, and so on, must be added. The client also needs to have a relationship with the subcontractor.

The details quickly become complex without a more formal language to represent the promises. If you want to see the formal solution, take a look at the mathematical treatment of Promise Theory.2 The result is quite beautiful in its symmetry properties.

It might seem surprising just how many promises need to be documented to accomplish something we do all the time. What is the difference between this and a chain of relative pushes? There, each agent merely throws a task “over the wall” to the next agent (this is sometimes called fire and forget).

If we use promises, our expectations are higher, but our blind trust that the outcome will come to pass is lower. We seek the assurance of a promise. This example represents the extreme end of obtaining maximum certainty through signalling of intent. For continuous delivery scenarios, this represents the minimum level of assurance for a process to be fully managed.

Trust is cheap, and verification is expensive. The cost of fully promised continuous end-to-end delivery grows as the order of the number of intermediary agents squared. This cost of an end-to-end assurance can be compared to the cost for the fire and forget approach, with only nearest neighbour assurances, which is linear to the number of agents. We thus see the up-front appeal of leaving things to chance.

End-to-End Integrity

In the worst case, one might make no promises between agents and simply see what happens. The usefulness of documenting and making all of these promises lies in seeing how information about agents’ intentions needs to flow, and where potential points of failure might occur due to a lack of responsiveness in keeping the promises.

A chain of trust is implicit in any collaboration, with or without promises. Without every agent talking to their dependencies and dependers, a service agent would not be able to detect if any intermediate proxy were to distort one of the promises. Thus the lack of trust in the proxies drives us to require more promises conditionally. This adds further overhead and expense.

In many real-world situations, one opts to trust agents without explicit promises, simply because this cost of tracking and verifying becomes too much to understand and follow up on.

In daily life, such promises are often made in a punitive form through legal contracts, under a threat of retribution for failure to comply with terms in a contract. This also turns out to be costly, as lawyers perform this exercise in a language of verbal warfare rather than cooperation.

Transformation Chains or Assembly Lines

The final generalization of the previous section is where agents do not merely pass along a service while maintaining its integrity; each agent also makes a change in the manner of an assembly line. This pattern is common in factories and product packaging companies.

A typical application for this kind of model is the scenario depicted in Figure 7-7, in which a product is designed and then built and distributed to customers through a delivery chain. Although we want to think in terms of this narrative, the reality of ensuring cooperation throughout the process is much more complicated. The same promise model sheds light on this case, too.

image
Figure 7-7. Schematic production line for goods or services.

Again, it is this issue of trust (or conversely the need for verification of intermediate steps) that leads to complexity in the promise graph. As more verification is expected, we approach a full set of promises between all of the agents, with every agent telling every other agent of its intent.

As intermediate agents promise more radical or sensitive transformations of data, trust in their promises by the point of service could become harder to swallow. Thus one can easily imagine that the most efficient delivery chains are those that make simple, harmless, and unambiguous promises.

Continuity of Delivery and Intent

When a system promises continuous operation, none of the promises may become invalid or be removed from the picture as a result of a failure to keep any promise (i.e., promises are described for all times and conditions, in a continuous, steady state).

Continuous delivery is what you get when you fly in a plane. The plane should not drop out of the sky while the service is being provided. As well as continuity of execution, there is continuity of purpose. When you fly in a plane, the plane should fly to a single destination and not be changing its mind all the time. Promises have to last as long as it takes to keep them, else they become deceptions. What makes a promise view valuable is that it allows us to define and document the virtual interface between client and server—or customer and provider—and to abstract away the helper agent. So what happens when we want to make a change?

Automation has become an important strategy for policing continuity of intent. Machines are excellent at validating rigid behaviours, without having to burden a human with a tight relationship with service specifics. We humans have a small and finite number of Dunbar slots to wield for our attention spans, and we want to spend them wisely. Machines can engage with mechanisms and humans can engage with humans (see Figure 7-8).

image
Figure 7-8. The promise of automation is to free up repetitive relationships whose only purpose is to assess inhuman promises.

In quality assurance schemes, product acceptance tests are used to create surrogate relationships that engage with a service to check whether it is keeping its promises.3 Such tests don’t promise quality, as such, rather they are trip wires that promise to detect unintended breakage of promises.

The Versioning Problem Again

As service providers learn and adapt to new situations, they naturally want to change the promises they make. This presents a conundrum for clients who are not ready to accept change. Service transfer requires there to be a matching binding between the give and take. In the worst case, promise bindings might be broken as a service agent fails to keep a promise on which a client relies. To deal with this issue, we use the concept of versions to keep parallel “worlds” alive without conflict.

A set of promises represents a product or service offering, and we want to be able to evolve that set of promises.

For uninterrupted, consistent experience, a user of promises expects:

  • Continuity of delivery (dynamics)

  • Continuity of intent (semantics)

But for long-term satisfaction, clients also want:

  • Continuity of improvement (changing semantics)

Many service users pay for improvements as part of the service. This is especially true in the software industry, where bug fixes are common, and it is actually possible to use a new version of a product after as little as a few minutes.

Avoiding Conflicting Promises by Branching into Separate Worlds

To avoid breaking existing products and services, the industry has learned to use version branching, so that we can pursue the illusion of being able to make many changes in one version without conflicting with another version. This is a kind of namespace.

Promising essential continuity of operation during a change is perfectly possible with a promise view of the world because each version becomes an independent agent, with its own namespace. In information technology, virtual machines and application containers represent such namespaces.

Recall that we define agents as parts of a system that can change independently. At any time, a collection of promises could be duplicated, like cells dividing, and then altered. This is the mechanism used in information technology for forking runtime processes, and also in the forking of development projects in version control (for example, git).

Users can choose which bundle of promises to accept and hence bind to, as long as multiple versions are kept alive (see Figure 7-9). Clearly, we can repeat this branching process as many times as we like. But the cost of keeping all the promises grows exponentially.

image
Figure 7-9. Branching of promises into different worlds avoids conflict. Different versions become separate autonomous agents, making different bundles of promises.

Avoiding Many Worlds’ Branches by Converging on Target

The faster we want to make changes to a service, the more versions we end up branching off. This could quickly become a problem. In product development, there is the concept of continuous delivery. This is about promising the semantic consistency and availability of every version of an evolving product design, as quickly as the changes are made.

If new branches were made for every small change, it would quickly pass beyond the ability of users to keep track of them, so an alternative strategy is needed. Instead of making evolving versions always coexist, to avoid conflicting promises, we can also make sure that changes preserve the same promises throughout versions by collapsing the multiple worlds back into fewer worlds (see Figure 7-10). In other words, we counteract divergence with convergence.

image
Figure 7-10. Climbing the version ladder, aka the stairway to heaven. To counteract the ladders, we can create snakes/chutes that collapse the branches back into fewer worlds.

Given a new intended outcome, we know that we can converge to a reliable product, without waste or conflict, as long as there are only weak dependencies between the components. Thus we never change any promised intentions. The only changes we should make would be to fix promise implementations, leading to promises that were made but not kept in earlier versions.

The idea of continuous delivery in product and service development is thus to de-emphasize thinking about major version releases that have to branch to co-exist, in favour of a continuum of incremental improvements that converge to a stable end state through an ongoing process.

Not all continuous delivery pipelines are about software development. The airline industry, for example, does a pretty good job of delivering continuous back-to-back flights by scheduling infrastructure slots. Schedules and flight plans change slowly, but adjustments may be made on the fly to adapt continuously to conditions.

Backwards Compatibility Means Continuity of Intent

So what we’re saying is simply that, if new versions break compatibility, then they break promises to users, and prevent users from accepting the new promises. If changes are made without knowing what the promises are supposed to be, there is no way to gauge whether they were successful or not because there is no record of intent.

Writing specifications is not the same as making a promise. A specification is only part of a recipe to change or build something; it still doesn’t document what the thing is for. So, if promises are not explicit, a service user has to go through a lengthy process of assessing changes to decide whether the new version is compatible with older versions, or simply whether it is useful.

Assessing a Service by Promising to Use It (Testing)

Assessing these service promises is the job of product testing. The likelihood of keeping promises is not improved by testing for correct outcomes, but by determining the correct intended outcomes in the first place. In other words, service quality comes from formulating promises, not from checking that they were kept. Nevertheless, testing is useful as a trip wire for unintended change.

Making promises up front separates intent from implementation for products and services. It separates design from execution and implementation. Version branching (divergence) is a tool to avoid conflicts of interest by keeping separate intentions apart. Continuous integration (convergence) is a tool used for avoiding conflicts by maintaining constancy of purpose. The former gives more freedom, but the latter is exponentially cheaper.

These techniques are widely used in software development today, and there seems to be some confusion about why versioning is important. The purpose of versioning is not to be able to undo missteps and allow developers to take less care in what they do. Intent is not transactional, but implementation might be. The idea of rolling back implementations is a piece of transaction processing mimicry. If we treat changes as transactions, then we make changes without intent. If we make a mistake in service delivery, we should correct the forward semantic process (i.e., the design), and the dynamical follow-up will maintain the target outcome.

A typical goal is therefore that newer versions would be compatible, drop-in substitutes for old versions. How do we know the design itself will converge to have the same fitness for purpose, with forward and backward compatibility? One way is to avoid too much branching and to correct continuously without change of intent. It is just basic maintenance, fixing promises that were not kept. As long as error correction is so fast that consumers can’t really see it happening, they will not be able to assess whether promises were not kept.

Componentization can help here (Figure 7-11). Modularity, or putting semantics into fixed buckets, helps convergence to a predictable set of end states.4 If your product is really a bundle of independent promises, then every promise can converge independently of every other, without interference or waiting. Even the order doesn’t matter, and parallel factories could be used to keep the component promises, to build the parts to be assembled later.

image
Figure 7-11. As we climb the version ladder, branching can be counteracted by ensuring that all the branches map into the same set of desired outcomes, or semantic buckets.

When every change version is backwards compatible (maintaining the set of compatible promises), you are converging, not breaking, earlier versions; and all changes reach a desired asymptotic end state, whether you are explicitly modular or not. It is a convergence of convergences, a semantic waterfall. Many worlds collapse back into one. Backwards compatibility is ensured when promises converge to a fixed, desired state and are kept continuously, even as the scope and details of the promises grow.

Some Exercises

  1. Imagine an online service, like a hotel reservation. Is the hotel run as a policy-based system, or as an API user interface? What are the promises made by the hotel? What is the API or policy declaration language? What proxies does the hotel service use to perform its services?

  2. Imagine a system of ordering a parking space in a parking lot by pre-arranged promise instead of by imposition. What would that look like? What does this analogy tell you about IT systems?

  3. Discuss to what extent a book can be viewed as a service. What role does the book play? What proxies are involved in the service (hint: is this an ebook, or do you wear glasses?)

  4. Think of an online music or video streaming service. What promises are made by the provider? What happens if the service becomes unavailable, is there a backup? What proxies are involved in the delivery of the streaming service?

1 This is not without its problems, and has been much criticized in recent years. In IT development and deployment, this led to the DevOps movement.

2 See Promise Theory: Principles and Applications.

3 CFEngine is an information technology automation tool that uses the concept of promises to embed testing of every promise at a fundamental level.

4 Here, computer programmers might think of hash tables or Bloom filters as tools for this kind of semantically stable vector of outcomes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset