Last Updated: February 25, 2016
·
434
· xaviervia

Generalizing Endpoints

It is quite easy to describe an HTTP API:

GET /users/:id

An HTTP verb and a pattern of the path it's all that is required. We call that conjunction an endpoint. Although the name is older than REST, and was in use for XML-RPC and SOAP web services, REST has clearly brought it to the mainstream.

Endpoints have proven to be very successful at tackling some standard challenges of distributed architectures:

  1. Clarity: They provide a clear, easy to share representation of the available interface.
  2. Semantics: By leveraging the HTTP idioms, they allow a structured representation of the application domain. This is specially true when exposing CRUD operations on application resources. The result is a very communicative API that can be grasped intuitively.

Endpoints and function prototypes

Endpoints can be likened to function prototypes. Take these two interfaces:

getUser(id)

GET /users/:id

If you think of the getUser function as a string pattern you can easily see that both are conceptually equivalent. If the function is consumed in a scripting language, the parallel is even closer since the name of the function can be constructed in runtime.

They are also really similar functionality wise:

  • Both have a descriptive name
  • Both have a set of possible arguments
  • Both will provide some sort of return value

Endpoints are much more expressive than regular method names. This is due to HTTP's features that allow them to represent different types of interactions. Observing this parallel make me pose the question: aren't endpoints some kind of generalization of function prototypes?

Isn't it possible to take the generalization of function prototypes to the logical conclusion?

Method calls as event-like objects

Another parallel that can be traced is between method calls and event emissions:

Target.write(text)

Target.emit("write", text)

The main difference between regular method calls and event emissions is that many listeners may be triggered by the event, while only one implementation of the method is allowed. It is obvious now how events are nothing more than a generalized application of the same central concept as methods.

Let's illustrate this further. We can rewrite the method call by sending an event-like object to an imaginary execute method in the core library of the language:

Core.execute({
  method: "write",
  object: Target,
  arguments: [ text ]
})

There is some stuff missing here, such as the scope in which the execution happens and the call stack, but you get the idea. In the end, implementing a method can be seen as nothing more than adding a callback to an event which is described not just by a name, but by a combination of properties of said event.

The generalization

The result of this exercise was building the Object Pattern library, and the Object Pattern Notation that goes with it. While implementing something along the lines of the Core.run method above is doable, it is doesn't have the kind of flexibility I was looking for. I imagined that, since we are already using REST endpoints successfully, implementing a generalization of the endpoint concept would be a crucial first step to getting a generalized event-driven architecture.

If you are interested, you should definitely check out the Object Pattern playground for more information on how to use them. I'll skim through the surface to explain how an implementation of an application resource (a generalized user management library for example) would look like in this architecture. I'll use HTTP idioms because they are familiar, but keep in mind that Object Patterns are designed to describe any kind of object structure.

Core.attach({
  "resource:/users/**": {
    "method:GET": function (event) {
      event //=> { method: "GET", resource: ["users", 24] }

      Core.emit({
        method: "PUT",
        resource: ["users", 24],
        body: { name: "Jennifer" }
      })
    },

    "method:POST": function (event) {
      event //=> { method: "POST", resource: ["users"], body: { name: "John" } }
    }
  }
})

Core.emit({
  method: "POST",
  resource: ["users"],
  body: {
    name: "John"
  }
})

In real life the architecture would also need a way to scope transactions (so that when you emit some event in the hope of getting an answer you can pinpoint which event was supposed to be the answer to yours) but as you can see, by abstracting endpoints we get to a generalization of several programming paradigms:

  • Generalization of OOP: resources act as objects that hold collections of callbacks that in turn listen to specific patterns instead of particular method names. The events themselves are also objects, but they are typeless, since what distinguishes an event from another is the properties that it contains (and consequently, the callbacks that it will trigger). Another way of seeing it is that the methods available to an event object are the set of functions that listen to the properties of that specific event object. Event-objects are both messages and data models. This duplicity of function is actually one of the goals of the architecture.
  • Generalization of event-driven, asynchronous architectures: executions are events, but the events are not scoped to a particular event emitter and they are not named or typed either. Furthermore, there is no difference between the event name and the arguments sent to the callback: very much like a UI click event, the event contains references to its whole context.
  • Generalization of in-app vs networked applications: given that the API is consumed via event objects and the answers are sent in the form of event objects as well, there is no difference in the interface for consuming a resource in the same application runtime or via a network. This is excellent, since scalability demands that application parts can be taken out and isolated with the least possible amount of friction.

Abstraction as the natural path of progress

The idea of objects with which you can interact in a uniform manner and that hold specific properties is a powerful model of reality, and one that drove forward programming for a long time.

The idea of actions happening as (more or less) global events to which several subscribers can react is another powerful idea that more recently helped in building complex, multipart applications such as GUIs.

The idea that data can be sent as messages in queues and processed continuously is another great idea, one that drove real time services in the last few years.

But putting all of those ideas together in a single application can become a giant mess. Designing an application with the competing paradigms side by side makes you spend way too much time debating questions such as:

  • Should this be method, or should it be an event? Should I make it an event for consistency even when there is only going to be one callback?
  • Is this function always going to be synchronous? Should I implement this as a promise or should it have a return value?

Abstraction is the path of less resistance when the paradigms converge. An abstraction that can encompass the present approaches will probably help out in the effort of building the next iteration of programming.

1 Response
Add your response

Should add tag "restful", "http"?

over 1 year ago ·