Service and API Design slides |

Partitioning into Services

Four Different Strategies to consider

  • Partition based on iteration speed
  • Partition based on logical function
  • Partition based on read/write frequency
  • Partition based on Joins

Iteration speed

  • Identify parts of the app which have stabilized and are not changing a lot
  • Versus the ones that are still being molded into final shape
  • It has to do also with whether they are at the “core”
  • For example with SocialApp: are the feeds and feed entries pretty much a given?
  • Would it make sense to partition them out into a separate service?

Logical Function

  • Intuitively partitioning based on logical function makes perfect sense
  • Especially if the logical function is distinct and separate from other parts of the product
  • In the social reader, the processes which follow RSS feeds and import them into feed items are very separate
  • This service would periodically access all feed URLs (of blogs etc that a user is following) to see if new items had been posted and if so import them and build the feed items.
  • If two users are reading the same feed (blog etc) this service would only need to import it once.
  • Similarly, there will be functionality to send out notifications. This is a natural and classic kind of service.

Read/Write frequencies

  • Directly out of the Baseball example we worked on
  • Data which is updated often and read much less often
  • Or, that is updated rarely but read extremely often
  • When the data is in different conventional databases this can benefit data consistency
  • Having one r/w database and a cluster of read only databases is a classic configuration
  • But using totally different kinds of storage, for example memory based and disk based
  • Or local versus cloud based

Join Frequency

  • Tables that need to be joined most likely should stay in the same database and managed by the same service.
  • But most tables are joined to another table at one point or another
  • Which is a good reason to go slowly when deciding to split data between databases
  • You might put table X which joins often with both table A and B, with table A because the joins with A occur much more often.
  • Sometimes it makes sense to denormalize, that is, store the same data in more than one place. That is duplicate table X in two databases.
  • This introduces the risk of data inconsistency which would introduce its own complexities

Versioning

  • Both external APIs and internal services should be kept as stable as possible
  • The reliance on an API is a kind of coupling which may bring about the very dependency which we are trying to guard against.
  • Service APIs should be versioned so that service A which accesses service X can specify that it is using version 1 of the API.
  • When service X updates the API they can maintain both version 1 and 2 until service X is updated to Version 2.

Version numbers

  • The system as a whole must decide on a version numbering scheme
  • A useful new standard for this is known as SemVer, Semantic Versioning
  • Within the codebase each service will include it’s current latest version
  • Importantly, each client of the service will include the version of the service they are assuming

What can be different?

  • New APIs can be added, old ones can be deprecated
  • APIs can get new arguments, or have the order of arguments, or the datatype change
  • The way arguments are supplied can change
  • The semantic meaning of the API can change
  • The format or datatype of the result can change

Caveat

  • Changing the API can be very disruptive to clients
  • It should be done very rarely
  • A non-breaking change is one that a client doesn’t notice
  • A Backward compatible change is a change that allows a new client to still safely use an old client
  • A Shim or Stub is an API that is kept around to support old clients, but just calls the new API

Version in the URI

  • Both http://www.nanotwitter/api/12/user/1 and http://www.nanotwitter/user/1?ver=12 are reasonable approaches
  • Using the URI query string has the advantage of making it optional
  • The convention is then that without the version, the service uses the latest

Version in the Accept: header of the HTTP request

  • A new mime-type is defined for the new version of the api
  • For example Accept: application/edu.brandeis.nanotwitter-v2+json
  • This would indicate that the request will accept results that are json and also application specific
  • But this technique is not recommended
  • While APIs in general operate over HTTP, there are different ways of structuring them

URIs and interface design

  • The starting point in contemporary applications is often REST structured URIs, e.g.
    • GET /users
    • GET /user/1
    • POST /user/1/tweet
    • PUT /tweet/12/text
  • The concept is that the URI refers to a representation of some resource
  • The resource would be user/1 which is a specific bit of information in the server
  • Stored in an unknown format
  • GET /user/1 requests, for example, an html or json representation of that user information

HTTP responses

  • Recall a response consists of a status code, a body and a set of headers
  • The HTTP standard defines a large collection of standard status codes
  • These are used in any HTTP scenario, including APIs
  • The most standard are
    • 200 OK means that the request succeeded and the data or action requested has been done
    • 404 Not Found means that the URI was not valid or recognized
  • The list is long and use is not super consistent
  • The body of the response normally comes as <%= link_to_topic :json %>, and is totally application dependent.

HTTP Caching

  • One of the advantages of REST structured APIs is that they benefit from system and network services
  • There are proxies and CDNs that will cache HTTP responses when conditions allow
  • The context is this:
    • the client has received a resource. The user clicks on a link for the same resource. Is it safe to just give them the one we have or should be go back to the server to ash for it?
    • These HTTP headers may be present on the request or on the response. Also remember that there’s the HEAD http verb which can query for information about the resource without receiving the resource itself. This is especially good for very large resources which rarely change.
  • Here are various HTTP headers involved:
    • Cache-Control: public Various settings that tell caching proxies what can and cannot be done
    • ETag: 737060cd8c284d8af7ad3082f209582d A unique key that corresponds to the specific “version” of a response. In other words, if the version on the server has the same ETag as the client has, then the response can be truncated to just the header. Or the server can request the current ETag of the resource and choose not to ask for the body because it already has it.
    • Expires: Wed, 02 Sep 2009 08:37:12 GMT The content returned by the server is considered valid up to that date/time. It doesn’t need to request it again until then.
    • Last-Modified: Mon, 31 Aug 2009 12:48:02 GMT Similarly, this indicates when the resource was last modified. If the server has a copy with the same last-modified date time then it has the latest and doesn’t have to request it again.

Handling Joins

  • As an example:
    • it would be useful if an API that returns a Tweet also return information from the user record as well
    • This way just one API call would be needed to retrieve information that is displayed in the Twitter timeline
    • If this were made into a separate service, then that service would need access to both Tweet and User records
    • In this “tweet” service, in order to really remove coupling it is tempting to give TweetService control of those two databases. It would do the join (efficiently) and clients of the service would get great performance.
    • However other parts of Twitter also need to access the Users. Do we give that job to the TweetService too?
    • That would make no sense!
    • This scenario brings us to the question of where joins should happen
    • How do you decide?
  • Here are the principles:
    • If joins are going to be common, or perhaps, even needed, it suggests that the tables should be together in the database
    • This is because the join logic when performed by the database is far far faster than anything you could do in the application server.
    • There are techniques to overcome this guideline but they are complicated and are very situation dependent.
    • Sometimes considering a non-REST approach such as GraphQL makes sense.

Evolving an API

  • Remember that an API exists to help the client
    • It is not at all unusual to add an API specifically to make a client operation more convenient or faster
    • APIs may be great places to handle errors, formatting, caching etc.
    • When you are designing an API for an actual client it is useful to start simple and add (YAGNI)
    • The simplest is just to create APIs for single models at a time, and have the client construct results that use info from more than one model.
  • As the client evolves, new requirements may become apparent
    • This model can be expanded by having APIs which include an explicit list of items or records, e.g.: /APIV1/users?id=1,12,33,1,22
    • This does not help with Joins. For example in the scenario above, how would the combined tweet/user API work?
    • When those needs arise it is time to create a new API which returns specific combined results on a case by case basis, for example `/APIV1/tweets?id=22,32,55,2&includeuserinfo=name

Summary

  • Partitioning an application into services is a serious design challenge
  • Its YAGNI vs. Performance
  • The general advice is to start with a monolith and break into services only when it’s clearly required