Skip to content

Deriving REST: Constraints

Adarsh Kumar Maurya edited this page Dec 10, 2018 · 1 revision

Overview

In the first module, we set the stage by establishing some of the reasons that would make REST an appealing choice in designing a distributed application. We also established the very high level definition for REST as an architectural style and then compare and contrast that REST to a few other styles as well as implementation technologies. In this module, we'll dive deeper into the RESTful definition. The goal of which is that you can understand how it is that REST is able to achieve the properties that we discussed in the previous module. The way that we'll accomplish this goal will be to firstly examine the approach used in defining REST. We'll then walk through each aspect of the definition so at the conclusion of the module you should understand the method for defining REST, its definition, and how its definition yields the properties or benefits that we discussed earlier.

Getting to REST

In his dissertation, Fielding begins his discussion of REST by identifying the method that he will use to go about defining it. He defines this method as being constraints driven as opposed to being requirements driven. A constraints driven approach is accomplished by identifying the forces that influence system behavior and then applying constraints so that the design works with those forces instead of against them. Now that can sound a bit abstract, so I've provided some visual aid to illustrate the difference between this constraints-driven approach and the requirements-driven approach. Take the aerial photograph of the Palm Islands in Dubai. This manmade archipelago was driven by a massive set of requirements. And while it's an incredibly impressive feat of engineering, it is also a constant challenge to keep the forces of nature at bay, no pun intended. Many software architectures are built in the same manner. We start out with a small set of business requirements and design for those. As we get new requirements, we grow our design to incorporate those. RPC designs tend to take this approach because the domain of the architecture and the domain of the business are tightly intertwined. Such designs typically focus on solving a programmer problem, for example to create an abstraction. They're conceived of and tested in a controlled environment and then deployed into the wild only to discover that there are critical limitations keeping them from being broadly usable outside of that environment in which they were designed. Conversely, REST was designed by identifying those forces in a distributed architecture that keep a design from being broadly useable. It then applies constraints to the working design to incrementally shape it. It is therefore the responsibility of the application designer to map the domain of the business on to the domain of the architecture. Put more succinctly, REST is different because it maps the business domain on to the architectural domain rather than mapping the architectural domain on to the business domain. So if REST is defined as a result of identifying forces that can be barriers to a distributed application, then understanding what these forces are can better help understand the significance of the individual constraints. As I was reading through Fielding's wok to try and extrapolate some of these, I realized that what I was reading lined up very cleanly to an article that a friend pointed me to a while ago, The 8 Fallacies of Distributed Computing. These are a set of assumptions that developers tend to make when building distributed applications that usually prove false and as a result yield a failed application. The list was originally created in 1994 by a guy named Peter Deutsch who worked at Sun Microsystems. The list consisted of 7 fallacies and was later amended to include one more in 1997 by James Gosling. I'm using this list of 8 fallacies to illustrate the kind of forces that REST was designed to take into account. I've also added one more of my own. The first is network reliability. The associated fallacy is that the network is reliable. It seems so obvious that this is not true. Power outages take down access points, people trip over network cables, hardware fails, we've all seen this happen. In my case, I was the guy tripping over the network cable. And even big supposedly robust cloud providers are not immune from reliability problems. For instance, both Amazon and Microsoft have had some pretty spectacular service failures in the last year. Yet we still see application after application that is designed without taking this lack of reliability into account. Latency, the associated fallacy is that latency is zero. If you've ever deployed an internet facing application where you have users in a country on the other side of the planet, then you possibly felt the pain that can be associated with this assumption. However, even if you don't need to support customers all over the world, it's likely that you will need to support customers who interface with your application via a mobile device. And for these customers, you must assume that they are on a network that is generally slow and frequently loses connection to the device. Bandwidth, the fallacy here is that bandwidth is infinite. And while we have much higher bandwidth capacity than we ever have, the reality is that there are still limits on just how much information we can fit into a network pipeline. And as it is with latency, this is especially true for mobile devices, especially considering that even if the customer has the available bandwidth, they may be incurring a nontrivial financial cost in order to use it. Security, the fallacy here is that the network is secure. This fallacy like so many others seems obvious when written out here and yet is so rarely accounted for in a real application. Network topology, the fallacy is that the network topology doesn't change, which in the world of the internet obviously doesn't hold up to even the most superficial observation. Servers and clients are in a constant state of moving around. This includes IP addresses, DNS names, even things that are managed by your server like relative paths and query strings. All of this change regularly as application scale and this hardware and software is updated and/or replaced. Administration, this fallacy states that there is only one administrator. Again, it should be pretty obvious in an internet facing distributed application that there is not nor could ever be a single administrator. However, so many times we design applications such that it is very difficult for someone not already intimately familiar with our application to manage it or diagnose an error condition. In fact, many times, when a remote service fails, you won't even be able to find an administrator for the failed service. Transport cost. There are a number of interpretations of the fallacy transport cost is zero. However, the one that resonates most with me could be restated as the cost of setting up and running the supporting network infrastructure is free. Stated like this, of course this is a fallacy. You have to pay for network hardware, bandwidth, plus different capabilities like load balancing. From an architectural point of view, the big idea that this fallacy addresses is that even if an architecture overcomes all of the other forces listed here, if you can't afford it, it's still not a successful architecture. Heterogeneous network, we spend a good bit of time reviewing this idea in the last module. But the fallacy is to assume that all nodes on the network are the same. Of course, we know that on a network as large as the internet, this can never be the case. However, the reality is that even on most corporate networks, we have to support a variety of different platforms and frameworks, particularly with the rise of so many mobile devices. Finally, I've added one more fallacy to this list and that is the fallacy of complexity. This fallacy assumes that everyone who uses our service has the proper level of domain knowledge and/or context such that they will use our service correctly. This fallacy can be seen in designs that require the client to send messages or call functions in a specified order.

Client-Server

So now that we've taken some time to establish the forces of nature that we want to architect with instead of against, let's start looking at the architectural constraints that define the RESTful style. We'll take a consistent method for exploring each constraint in turn. First, we'll look at the constraint itself. Then we'll see what forces the constraint addresses. And finally, we'll see what properties come out of applying the constraint. We'll start with the client server constraint. This constraint defines all interactions between nodes in a distributed architecture as being between a client and a server. The client initiates by sending messages to the server for processing. The server listens for messages, performs some processing, and returns a response to the client. The goal of this constraint is separation of concerns. By separating the concerns of the server and client, particularly for things like user interface, it means that lots of different types of clients can work with the same server. It also means that clients can evolve without impacting the services that they consume. Again, the key principle behind the client-server constraint is separation of concerns. This acknowledges and addresses a couple of the forces described earlier. Firstly, complexity is managed by the fact that clients know only about servers and not one another. And servers don't know anything about the clients that send requests. This separation between clients and servers makes it much more possible to support a heterogeneous architecture as clients and servers can be built on completely different platforms using completely different language tools and frameworks. Applying this constraint also addresses concerns such as administration and security, and then it provides a natural scoping boundary in the interactions that need to be secured and managed. It also lays the foundation for the layered architecture constraint that we'll talk about later in this module. Applying the client-server constraint provides quite a few benefits and the properties that it yields. A few of the notable ones are, portability of clients, as we've described separating the roles of client and server means that we can have lots of different types of clients that consume our services. Scalability is achieved by simplifying the server components, making it easier to add new ones as load demands drive this. Also, as I mentioned above, applying a separation between clients and servers enables clients to evolve independently from the servers and services that they use.

Stateless

The next constraint that will apply on our journey of defining REST is statelessness. The stateless constraint has been around a long time but was given a particular place of prominence in distributed applications. In fact, I remember hearing about it for the first time in the days where COM (phonetic) components were going to solve all problems. At any rate, the big thing to note here is that this constraint is not trying to get you to create an application that has no state. This kind of application would be incredibly boring and unhelpful. Instead, the stateless constraint here applies to the communication between clients and servers. To elaborate, the stateless constraint means that the server should be able to get all the information that it needs in order to process a client request from the request only, and not from any additional context information that it is responsible for maintaining, such as, session state. In a system design where the stateless constraint is applied, all session state is maintained on the client. As you can see in the illustration here, this means that a workflow can progress between a client and any number of back-end servers without needing to worry about things like the workflow state being corrupted as a result of synchronization problems between server nodes. The stateless constraint is all about dealing with the reality that a distributed application exists in a world where the ground is constantly shifting out from under the application. Sometimes, this metaphorical ground is actually there and the request from a client can be correctly processed by a server and return safely to the client. However, this is by no means guaranteed and many times, the client crashes, the server crashes, or the network becomes unstable before an entire request and response workflow can complete. This is even truer for longer running workflows that are made up of multiple requests and responses. Similarly, the stateless constraint is well suited for an environment where clients and servers are constantly being added and removed where IP addresses and DNS entries are constantly changing and where intermediaries come and go based on the network path selected for an individual message exchange between client and server. Additionally, by making state explicit in the communication between client and server, that state is also available to intermediaries, which can include management applications, such as intelligent gateways and routers. We'll talk more about some advanced possibilities for smart intermediaries when we talk about REST in the cloud. But the key point here is that the stateless constraint is what makes this all possible. Now let's look at some of the benefits that the stateless constraint can provide. First is visibility, this is the overarching property gained by applying the stateless constraint, and it's frankly the property that enables many of the other properties that we'll talk about. Visibility enables all architectural elements that work with requests and responses to process the messages in isolation from one another without risking state corruption. Next is reliability, in my opinion, this is the greatest benefit that statelessness can provide. This is because the state of a given workflow always exists in one place at a point in time. As a result, any failure in the client, the server, or the network, can be recovered from in a deterministic way. For example, a failure on the server can be compensated for by the client updating it with the current state. A failure on the client can be compensated for by getting the last known representations from the server. Typically, moving the client back one step in the workflow but not corrupting it entirely. Now, compare this with a design that spreads the workflow state between clients and servers using something like session state. In that scenario, if either the client or the server fails, the state of the entire workflow is unknown and the workflow should be restarted. As you consider scaling this scenario by adding multiple server nodes that must all have synchronized access to this shared session state, the probability for corrupted state only increases. By constraining communication to be stateless, the application can be scaled out by simply adding new servers capable of processing the requests.

Cache

I think that many people associate REST with caching because APIs that use HTTP well end up being cacheable. However, caching is actually a part of REST's definition. Specifically, and this follows the previous constraint of statelessness nicely, the cache constraint says that responses from a server must be explicitly labeled as cacheable or not cacheable. Having these cache declarations as a first class part of the message means that a RESTful architecture can benefit from multiple levels of caching, including server caching, client caching, and caching on any intermediary that may sit between the two. For example, consider a typical web application. Responses can be cached in the browser on your company's proxy server or anywhere along the path to your origin server. In this case, it's generally pretty easy to tell whether a request was served from your local browser cache. As in this case, you'll see no request issued and the HTTP status code of the response will be 304. However, you can also tell when a response is served by an intermediary cache such as a proxy server by looking at HTTP's via header. The forces that guide the application of the caching constraints are all related to the characteristics of any Real-World network. For example, because network latency is never zero, caching enables clients to completely avoid making some requests and waiting for the responses. Similarly, because bandwidth is never infinite, getting the data as close to the client as possible can reduce the number of network segments that data needs to be pulled across. And ideally, the data is cached locally so that the client doesn't have to consume any network bandwidth. Finally, the cache constraint acknowledges the fact that there is absolutely a cost for both the client, and the server with regard to a network interaction. The client for example may be a mobile device where the user is being charged for the amount of bandwidth consumed. Similarly, the server infrastructure can only sustain a discrete amount of load before it must be scaled, which naturally comes at a cost, sometimes a nontrivial cost. Caching addresses these realities, by providing a mechanism to reduce the number of requests that need to be sent all the way to your origin server in the first place. As such, caching makes your design more efficient with respect to both latency and bandwidth. These efficiencies then make your design fundamentally more scalable and that your application simply handles more clients. Put another way, your application is able to scale out using the infrastructure of the entire network rather than simply scaling out by growing its own resources. Finally, by caching representations close to the user, perceived performance can be dramatically improved as the representation is instantly available.

The Uniform Interface

The Uniform Interface is really the key differentiator between REST and many other architectural styles. In the early history of the web, consistency between all of the different processing nodes, clients, servers and intermediaries was maintained only because all of those nodes use the same implementation library, CERN's libwww. The designers of the web quickly realized that enforcing consistence semantics by way of a specific implementation, which was of course coupled to language platform, et cetera, that this was not a scalable solution, and therefore the Uniform Interface constraint was developed and applied in order to help the web scale quickly and reliably. The purpose of the Uniform Interface is to apply the more general software design principle of generality to the communications between distributed components. And as you can see here in the diagram, applying the Uniform Interface constraint means that all the nodes in this design, whether they're a client, a server or anywhere in between can communicate with one another via a standard mechanism. ( Pause ) Now the entire next module is devoted to the Uniform Interface. But because you'll hear these terms used a lot in reference to REST and even HTTP, it makes sense to provide a basic definition here. First, a client consumes service capabilities by interacting with 3 sources. Resources are identified by resource identifier, in HTTP terms this is typically a URL. While a client interacts with resources, the client works directly not with the sources but with representations of those resources. For example, I am a person and I can be identified by something like my name. However, if you're a software program, you won't fetch and work with me, at least I hope not, instead you work with an XML or JSON representation of me. In addition to the requirement that communication in a RESTful system be stateless, the constraint of self descriptive messages means that a message should also contain the metadata needed for a client or server to actually do something with it. Put another way, statelessness makes the state available to the components. Self descriptive messages make the state intelligible. Finally, the hypermedia constraint gives servers and clients a way to truly be decoupled from one another by enabling clients to progress through a workflow by following server generated links, much, much more on this in the next module. Like I mentioned, the big deal behind the Uniform Interface is applying the principle of generality to network interactions between distributed components. This is in response to several of the forces mentioned at the beginning of this module. The first is that the network is not reliable. Therefore, it's important that all components involved in processing request and serving a response understand message semantics in the same way. This is so that they can both take advantage of optimization opportunities, as well as gracefully handle error conditions. In addition to the network being unreliable is the fact that the shape of the network is constantly changing and providing a Uniform way of handling communication enables new innovations to be developed and tested, as well as enables existing components to be reliably improved. The reality of decentralized or nonexistent administration is addressed by the Uniform Interface and that the semantics of any interaction are well known to any node on the network, enabling mitigations to be developed in the event of a failure, even without having a phone number to call. At the very least, the semantics defined by the Uniform Interface should tell the person handling the failure whether or not they need to search for that phone number. Most importantly in my opinion, the Uniform Interface acknowledges the reality that applications on the web are written in all sorts of different programming languages and they run in all kinds of different environments, different platforms and on different frameworks. Ensuring that all those need to only understand the Uniform Interface has in my estimation been the principal reason for the exponential growth of the web. Finally, and closely related to that previous point, the Uniform Interface acknowledges the reality that RESTful applications will consist of components written by lots of different people in lots of different ways. And that ensures that no component in a distributed application needs to understand the internal implementation details of another component. To put it in service-oriented architecture terms, the Uniform Interface is the contract between clients and servers. Now there are ton of benefits that the Uniform Interface provides, pretty much every force that the Uniform Interface addresses brings along with it more than a couple benefits. However, 2 that I'll call out here because they're a kind of umbrella benefits are visibility and evolvability. Visibility is that property that ensures that a message means the same thing to every component involved in processing it. There are a ton of benefits that fall out of this, from the ability to reliably innovate, to the ability to support an unbounded number of different platforms. Evolvability is a property of the Uniform Interface that enables all nodes in the system to be improved or replaced entirely without causing the system to become unstable. To summarize, the Uniform Interface constraint is a really big deal, so big in fact that we're going to spend the entire next module focusing on it.

Layered System

Earlier on this module, we talked about how the stateless constraint improved visibility for all nodes involved in the communications between clients and servers and that improving visibility improved things like reliability and scalability. We then apply the Uniform Interface constraint and gain generality, ensuring that all components communicate with one another in the same way. We can then build on this foundation and apply the layered system constraint to help further manage complexity and thereby scale the reach of our design, pass the boundaries of what could be manage by any single group of people. Similar to that of software component architecture, the layered system constraint says that a component in a system can only know about the components in the layer with which it is interacting. Applying this constraint places a fundamental limit on the amount of complexity that you can introduce to a system at any given layer. The tradeoff for applying this constraint is that you can introduce latency, since after all, strict layering means that a message will likely have to travel through more intermediary nodes than in the case of a direct connection. This can be mitigated, however, with specialized intermediaries such as shared caches and load balancers as shown here. Again, the layered system constraint is driven by the principle of simplicity and in context, simplicity here is directly related to the reality that the network infrastructure between and including the clients and servers of a distributed system are constantly changing. Enforcing strict layering limits the scope of impact that even significant changes in one layer can have on all of the other components in the system. Additionally, a layered system addresses the reality that the network is not secure, and that each layer represents a potential security boundary whereby an administrator can mange unique security policies for the components in that layer. The natural benefit that comes out of applying the layered system constraint is that the whole system can scale far beyond the limits of any system that is managed by any centrally managed group of administrators or resources. The modern web is a prefect illustration of this benefit in action. The web has no central administration. My web browser is managed by me. It knows how to communicate with my corporate proxy which is managed by my company. That proxy knows how to communicate with its internet service provider which is managed by some other telecommunications company and so on. Each layer of the system is managed by different people and has different policies. But as a result of the Uniform Interface, they all communicate in the same way and because of this, I don't need to know or care about anything past how I point my browser at my proxy server, and it is this combination of simplicity in scale that enables me and 2.2 billion other people to all connect and use the internet.

Code-on-Demand

The code-on-demand constraint doesn't end up getting talked about as much and talks on REST, in large part because it's listed in Fielding's paper as an optional constraint. And it's optional for good reason, and we'll talk about that in minute. That said, I also think that it's been ignored somewhat because there were several years were there wasn't a technology that proved ubiquitous enough to make it worth applying it, at least not outside of an internal more centrally managed system. However, with the continued growth of rich web client applications, think HTML5, jQuery, AJAX, et cera. And with the emergence of JavaScript applications which are independent of a web browser, think node. The code-on-demand constraint may prove advantageous as it relates to script. Before I get too far ahead of my self though, let's define the constraint. As shown here, the constraint says that in addition to delivering data and metadata, servers can provide clients with executable code. The primary force driving the code-on-demand constraint is complexity. And therefore, the primary benefit provided by the constraint as I already mentioned is simplicity. The idea is that clients can download readymade features and not have to write special and possibly redundant logic. Now all that said, the benefits provided by the constraint don't come without tradeoffs. And the tradeoff here is a pretty big one. Having the client download and execute readymade features can greatly reduce visibility which can have a negative impact on things like caching and manageability. Not to mention the security risks associated with downloading and running code that you may not control. The key takeaway for what to do with this optional constraint is this. It's fine to apply code-on-demand. However, you should apply it in such a way where it adds value to the design for clients who support it, but then it doesn't break clients who don't support it. A good example of the constraint is found in the world of passive identity federation. Think of the case where an application delegates authentication responsibilities to an identity provider like Google or Facebook. As a part of a federated workflow, one server returns a response where the expectation is that the user agent will issue an HTTP post to another server. Because a post request can't be initiated as the result of a redirect, the server returns in the response and HTML form along with some JavaScript code to automate submitting it. However, in the event that script support is disabled in the user agent, the response also contains instructions so that a user can submit the form. Again, clients that support code-on-demand are simplified, while those that don't have an alternate way to make progress.

Summary

In this module, we've gone deeper into definition of REST, seeing how it's the result of incrementally applying constraints rather than building it up from requirements. We then explored each of the constraints in detail, looking at the constraint, the forces that guide its application and the properties or benefits that it can provide to the overall architecture. In the next module, we'll see how these constraints, particularly the Uniform Interface, form the vocabulary that we'll use in designing RESTful systems.