Skip to content

Notes on V3 Architecture

Greg Cobb edited this page Nov 30, 2018 · 8 revisions

This article is about code design/ architecture for the v3 API.

For API design, see the API styleguide.

For Asynch/background jobs see: v3 asynchronous endpoint flow

Preamble

The v2 API was architected to allow rapid generation of API endpoints by a small, dedicated team of developers. Now that Cloud Controller is a mature, open-source project with hundreds of contributors throughout the world, these architectural decisions have resulted in a few issues:

  • The framework that v2 is build on is powerful and semi-flexible, but is not maintained as a stand-alone tool so it is not used by any other projects or documented for common use. This makes it difficult for new developers and open-source contributors to learn its numerous eccentricities.
  • V2 endpoints are largely auto-generated, so it can be difficult to keep track of what endpoints exist. It can also be difficult to build endpoints that have custom behavior, not supported by the framework.
  • V2's reliance on model hooks results in a distribution of business logic across multiple units. This has a number of implications. First, it can be difficult for developers to follow and modify user workflows through the various hooks. Second, the hooks can result in unexpected, emergent behaviors for seemingly unrelated changes. Third, testing user workflows is difficult outside of top-level API tests.

The code design of the v3 api is a direct response to these issues. For example, the v3 API:

  • Uses several Rails libraries instead of a custom framework
  • Favors explicit vs generalized or meta-programmed code
  • Organizes business logic around user workflows (actions) instead of domain objects

Code Organization

Message Object

Message objects run superficial validations of user input. For example, validate JSON structure, check for unexpected keys, and verify types of parameters/ data. They also serve as a value object to be used by subsequent objects in the request cycle.

Responsibilities

  • Run validations on the request that do not depend on database state
  • Provide data from request to later collaborators in the system

Fetcher Object

The fetcher object’s responsibility is to get record(s) from the database. It also handles filtering based on user input. Fetchers are only used for endpoints with filtering or other complex query logic.

Responsibilities

  • Retrieve records from the database
  • Apply pagination and filters

Action Object

Action objects contain most of the business logic and typically implement user-facing workflows. Common examples include creation, deletion, or updating of a resource. Action objects make it easier to understand, test, and modify the v3 API's behavior.

Action objects may or may not be shared between v2 and v3 endpoints. Actions may be composed for more complex endpoints.

Responsibilities

  • Implement context-independent business logic
  • Collaborate with other actions to build compound workflows

Presenter Object

The presenter object handle rendering the response for the user’s request. It implements a to_hash method that when supplied to the render method in ActionController, results in rendering JSON.

Responsibilities

  • Structure response data
  • Make superficial presentation changes to the data
  • Pagination
  • Include links to related endpoints

Models

Models that are only exposed on the v3 api have very few responsibilities. They model associations between resources, provide convenience methods for accessing their data, and have basic validation to detect invalid states.

Responsibilities

  • Write/read/update the database
  • Provide convenience accessors
  • Manage associations between resources
  • Validations based on database state
  • Rarely: Use model hooks to update external systems when states change

Controllers

Controllers should coordinate between other objects, but contain little to no logic of their own. Controllers should be a gateway to API-agnostic behavior, and not implement any of it themselves.

For a standard controller, the request will go through 5 layers of scrutiny:

  1. Middleware will make sure that the user has a valid UAA token
  2. A Message will make sure that the request body is syntactically valid
  3. Permissions will make sure the user has access to the resources they are trying to act on. This must be after the message, because permissions are currently tied to orgs/spaces, which are often specified in request bodies. This check must depend on only the user's auth info and the space/org of the resource in question (no business logic).
  4. The action will make sure the request conforms to any business logic validations (no creating paperclips on Tuesdays!)
  5. The model/database schema will run validations that depend on database state (uniqueness validations). This is important to prevent/narrow race conditions around database state.

Responsibilities

  • Collaborate with Message and return any errors
  • Collaborate with Permissions and return any errors
  • Collaborate with Fetchers
  • Collaborate with Actions and return any errors
  • Collaborate with Presenters and render their results

Shared Objects

Permissions

Responsible for determining if a user has access to a give resource based on their roles and scopes.

Composition Diagram

Architecture

Current Quibbles

  • Actions taking messages as inputs reduces the portability of actions. If you want to use them outside of a v3 controller, you must create a message object by hand. Maybe actions should not take messages as arguments?
  • It is currently ambiguous how much Messages care about structure of the request. They are de-facto responsible for parsing requests, but then also implement structure-agnostic data validations.

TBD

  • Code examples
Clone this wiki locally