API layers - or "Where do I put my logic?"

We are now well and truly in the age of the API, however I feel that there is still a lot of confusion about the best patterns to put in place in how to structure APIs,  There are a number of competing patterns with some overlap and plenty of trade-offs, particularly in the space of building digital systems where the MACH alliance is gaining momentum.  Some of the patterns you might have heard of include Microservices, Backends-for-Frontends (BFF) and domain services.

I personally prefer to think about structuring APIs in terms of the different types of logic the a system composes of and how this impacts the trade-off between pace of change/innovation compared to reuse and stability.  These two forces are in constant tension in enterprise level development, and is well captured in Gartner's Pace Layered Architecture concept.

Layers of logic

I like to categorise the logic implemented a system into four different layers, as shows in the diagram below.

(1) Presentation Logic

Presentation logic is responsible for laying out the UI of the application for end users to use.  

It does not perform business or application logic that transforms the view models.  It does hide and show UI components but ideally uses view model values directly rather than performing complex calculations in the UI (this helps with testability of application logic).

(2) Application Logic

Application logic that controls the way that information is manipulated by one application in particular, but implements rule that don't need to be shared between systems.  Having application logic separated from than the UI helps with test automation, and keeping it separate from domain logic allows for changes in individual applications to happen much more quickly and with greater confidence as they aren’t shared between multiple applications

(3) Domain Logic 

Domain logic models key domain information and enforcement of important inviolable domain business rules.  Domain logic needs to be shared between applications, so changes have a higher impact across calling systems.  This means that changes to domain logic need to be more considered, more thoroughly tested and handle issues like versioning or backwards compatibility as not all client applications will move in synch with the domain layer.

There is another type of reusable service that provide orthogonal services within the platform that are used by, but that isn't really domain logic.  These are shared between multiple domains and often wrap some infrastructural concern such as logging or authentication.  These aren't exactly domain or integration logic, but I like to consider them as "technical domains".

(4) Integration Logic.

Integration logic deals with synchronising data, usually between enterprise systems.   It supports features such as retries, publish/subscribe patterns and where required things like batch processing to ensure separate domains or underlying systems remain synchronised.  This isn't implementing domain based logic but is handling the important plumbing that makes integration work.  

Things like API gateways in enterprise systems that implement policies such as caching or throttling I also consider a type of integration logic.  I'm generally against implementing policies that transform message payloads in these types of systems (although sometimes mechanistic transformations like SOAP to REST might be valuable, I generally think that any sort of complexity in such a transformation is better achieved with a proper logic service)

Theory is all good, but what does that mean in practice?

Each of the logic layers outlines above I generally would map to a separate layer in implementation, with Application Logic and Domain Logic deployed into separate sets of APIs.  I like to think of each of the layers in terms of it's responsibilities, along with what interfaces they generally provide and what components in a system would call them.

Presentation components

Exposes

  • User interface (HTML via ReactJS, CSS, Images etc. for Web, Native mobile application code for mobile)

Responsibilities

  • UI Layout
  • Making calls to APIs to get data (one call per API component unless needing to perform operations like polling to drive UI behaviour)
  • Binding the view model
  • Hiding or showing UI components based on the view model.
  • Accepting and responding to user commands, including routing between screens

Called by

  • User

Consumes

  • Application logic APIs (BFFs / Experience APIs) 
  • External APIs or widgets (in some cases, e.g. Google Maps, Twitter feeds)
  • Sometimes Domain APIs.  In some simple cases where there is little to no application specific logic the overhead of application logic APIs might not make sense, although I'd generally prefer to have them as a layer where application logic can be added if and when if occurs - otherwise the temptation is to put it in the presentation layer)

Expected change cadence

  • Application speed (Fast - days)

Notes

  • Specific for a particular application, although shared components may be built (although these are considered mini-applications in their own right and composed into larger applications, which means they might have their own application logic APIs)

Application Logic APIs

I generally use the terms Experience API and BFF interchangeable for APIs that house application logic, although I think that the BFF pattern provides the best template for how these should be designed.  

Exposes

  • Application/page/component specific view model (JSON described by OpenAPI contract)

Responsibilities

  • Validating the security tokens passed from the UI and ensuring the user is authorised to make the requested call
  • Building the view model that will be used to bind directly to the UI
  • Application specific logic such as calculations to build the view model, that don't need to be used in other channels
    • Orchestrating calls to multiple domain APIs to build a consolidated view model needed by the calling UI component (e.g. merging a list of all investment options from reference data, with a list of balances for held investment options from Link)
    • Filtering the fields returned in a view model where all the data in the domain model is not needed, and the extra fields would present a security, performance or data usage concern.
    • Caching view models that are expensive to create
  • Responding to application specific commands issued by the UI
    • Validation of view model submitted
    • Transforming back from a view model submitted by the UI to the appropriate domain model for using the domain API where the view model is different and only used in the one channel
    • Orchestrating calls to domain APIs to perform application specific logic (e.g. update a domain model, and then send a notification that should only occur if performing the operation in this one channel not consistently across channels)

Called by

  • Application UI (running in user browser/mobile application in a device)
  • Could be used to provide APIs for other systems that need channel specific logic implemented (e.g. if a custom view/data transfer model orchestrating several domain services needed in a single call from UBS)

Consumes

  • Domain APIs
  • External APIs (where the external API is tied directly to application logic and not shared in other channels)

Expected change cadence

  • Application speed (Fast - days)
  • Likely to be deployed together with changes to the UI

Domain APIs

Domain APIs provide a platform for fast and agile development of applications by providing consistent and dependable encapsulation of domain logic. I prefer to use the tenents described in Domain Driven Design to model domain APIs around the ubiquitous language of the organisation, although it is important to note that Domain APIs should be designed to be used across an organisation, which means that if needs to hide the complexity of the domain but still provide enough detail for a wide number of client applications.

Exposes

  • Domain model (JSON described by Open API contracts)

Responsibilities

  • Validating security tokens passed and ensuring caller has permissions to access/modify requested data
  • Accessing source systems (APIs, Databases etc) to build a canonical domain model with data that is shared between all channels
  • Implementing calculation and other transformational logic that needs to be applied consistently irrespective of  the calling channel
  • Orchestrating calls to multiple systems to gather required information to build the canonical domain model
  • Orchestrating calls to systems to implement business rules that needs to be applied consistently irrespective of calling channel (e.g. store a switch request with Link, and the call notification service to send an advisory email consistently irrespective of calling channel) 
  • Orchestrating calls to multiple systems where changes need to be applied transactionally (all or nothing)
  • Encapsulating integration logic for accessing external APIs (e.g. if needing to use service bus to make asynchronous calls to Link to submit an update)
  • Caching domain models that are expensive to create

Called by

  • Application Logic API
  • Application UI (although not recommended)
  • Could be used to provide APIs for other systems including externals where they are trusted to perform operations at this level

Consumes

  • Other Domain APIs
  • External APIs (e.g. where third party information is being used to enrich the domain service)

Expected change cadence

  • Enterprise speed (Medium - weeks/months)

Notes:

  • Domain APIs should reflect a model for business domain concepts, and provide a canonical model using the ubiquitous language terms used by the business.  

Conclusion

When designing API based applicaitons, I prefer to implement a layered architecture that separates the different types of logic found in the system - presntation, application and domain.

In particular I like to follow the BFF pattern for implementing application logic, allowing the applicaiton logic to be captured in APIs for better testing but deployed quickly (applicaiton speed) and with minimal risk. Where logic needs to be shared between applications (or channels such as by a website and a mobile application) I prefer to model this in a Domain API which is updated more slowly and carefully taking into account service versioning and backwards compatibility.

Comments

Post a Comment

Popular posts from this blog

Sitecore - multi-site or multi-instance?

Setting up TDS to work with Azure DevOps

Cloud hosting Sitecore - High Availability