Layered APIs

autarch on 2007-03-12T16:28:55

At Socialtext, many of the developers kept internal blogs for a long time. This is a cleaned-up version of something I wrote there that I'd like to make public, because I won't have access to it after today, my last day at Socialtext.

Some background ...

This was written back in March of 2006, when we were talking about how to release most of our code base as open source, and trying to figure out what pieces we could hold back as proprietary enhancements. The term "fridge" is a term Chris Dent used a lot, which is the idea that our system should have simple interfaces, like a kitchen appliance such as a fridge.

------------------------------------------------------------------

This is something I've been thinking about for a while, primarily because of some thoughts I had when discussing what we could leave out of our OSS offering, and how hard leaving out certain bits would be. I suspect that this has already been described by someone somewhere, but searching for things like "layered api" or "tiered api" on Google does not seem to come up with anything interesting. Maybe I'm just not using the right terms. Anyway ...

In trying to make each piece of our system more like a fridge, I'm seeing distinctions between various layers of the system, kind of like this:

  • UI
  • Services (verbs)
  • Data (nouns)

The bottom layer, our foundation, is the API we use to for getting and changing data. This layer consists of nouns like "user", "workspace, "page", etc. The classes in this layer are responsible for telling us information about a specific type of noun, and for updating information about those nouns. Nouns depend on the presence of other nouns. For example, in my branch, Socialtext::User depends on Socialtext::Workspace and vice versa. A simple example is that workspaces have a creator, which is a user. Workspaces also have members (with roles), and those members are users.

These noun classes tend to be fairly dependent on each other in general, and I think that's ok. These classes are all at the same layer, and I think we should expect all of the pieces they represent to be present. In the context of "what should be left out of the OSS offering", I'd say nothing from this layer should be left out. This is the core of our app, and trying to make these pieces pluggable or optional causes cascading complexity throughout all of the higher layers, because they have no stable foundation to build on.

On the flip side, I suspect it's probably important to make sure that the bottom layer doesn't depend on anything from higher layers. The user class should not depend on the authorization checking service, though the opposite dependency may exist.

The next layer is services, which are verbs. For example, the formatter is a service, checking authorization is a service, etc. Services will depend on specific pieces of the foundation layer. For example, the authorization checking service depends on the user noun, and possibly on other nouns like workspace, account, role, etc. Services should be more easily pluggable. Again, with authorization, we might have one authorization service that checks against the DBMS, and another that checks against LDAP. We could also have an "always allowed" checker that just says yes to everything.

Some services may depend on other services. The formatter depends on the authorzation checker to determine how to format inter-workspace links, for example. Rather than removing a service entirely in the OSS offering, we probably want to replace it with some sort of no-op plugin version. For authorization, that'd be the "always says yes" checker. This allows us to say that a service depends on an API of some other service, without depending on an implementation.

Finally, at the top layer, there are the UI bits. I'd include things like webservices here, and maybe this should be renamed "outward-facing interfaces". These are the interfaces that are exposed via network protocols or command-line tools, as opposed to via API calls in Perl. These interfaces will rely on both services and data. For example, our "heavy" HTML layer uses both services (auth checking, formatter) and data (user, workspace). Each of these layers should be independent. It just seems wrong for the webservice layer to require the HTML layer to exist, or vice versa.

In terms of leaving pieces out of the OSS release, this is the easiest layer to leave out. Nothing in the code base depends on any particular external interface, so it's absence does not require any special coding.

Each of these layers may in turn be made out of layered piece as well. For example, on our bottom layer we have a DBMS, Alzabo as a low-level querying/updating interface to that DBMS, and then the Socialtext::* data classes, which provide a higher-level API for that data.

I think that layering describes a good architectural goal for our app, one which encompasses the idea of componentization quite well, or maybe it's just another take on componentization. Each layer should be made up of well-defined components separated by known APIs. It also helps me think about how to handle dependencies, and highlights tricky areas. Some questions I think are interesting:

  • What level of inter-dependence is okay at the service layer? How can we minimize dependencies at this layer while still keeping the code clean?
  • Is it okay that the top layer (UI) often uses the bottom layer (data) directly? On one hand, this seems "unclean". On the other, if we had to have a service layer object for everything we wanted to do with data, we'd have an enormous amount of code, since we'd need querying and updating services for all of our data.
    • What does seem to be valuable is adding small services between the UI and data layers in selected areas, like authentication and authorization. Having these as services means that we can make them pluggable, and do them via LDAP, for example. I think there's a middle ground between too many services, which means lots of indirection and extra code, and too few, which makes our app inflexible in the face of certain user requirements.
  • What do we do if we think we need for a layer to depend on a higher layer. For example, what if something in the data layer needed a service? For example, what if we want to have the app update group membership data in LDAP?
    • One possible solution I can think of is to allow for explicit hook points in the data layer, and then hook in calls to services.
    • Another solution would be to move all of the updating APIs to the service layer, so that we had an authz updater as well as an authz checker.