Cloud Controller is the primary API through which third parties interact with Runtime; it encapsulates the myriad internal services that take a user from ‘cf push’ to a running application. Both the Pivotal Developer Console (the web application that ships with Pivotal CF and backs console.run.pivotal.io) and the CloudFoundry CLI interact directly with the Cloud Controller API. The two applications were originally written in Ruby and shared a common Ruby code base for talking to Cloud Controller called CFoundry.
CFoundry provides a friendly interface to the Cloud Controller API. The library encapsulates the logic around endpoint requests, object relationships, and error handling, permitting new users to intuitively navigate the object graph by working with domain objects rather than raw data. The CLI was recently rewritten in Go, and as such, no longer has a dependency on CFoundry. Our team, responsible for Pivotal Developer Console, took this as an opportunity to re-evaluate whether CFoundry was the right choice for our web application.
As Developer Console complexity grew, we found we wanted to be ‘closer to the metal’ so we had the ability to fine-tune the requests we made to the Cloud Controller API. While it is technically possible to make nearly any request that the Cloud Controller API supports via CFoundry, the interface does not encourage it. Further, although Developer Console domain models are largely backed by Cloud Controller data, they also incorporate information from both UAA and a relational database. New developers on our team were frequently confused by the difference between objects returned by the CFoundry gem and those used in the Rails application. Lastly, CFoundry was tightly coupled to the behavior of the domain objects in Developer Console which made it challenging to stub domain object values in integration tests.
We decided to gradually remove the use of CFoundry in our app and replace it with a new pattern the design of which would be guided by our CFoundry experiences. We wanted this new pattern to exhibit a couple of key characteristics:
1) It should be easy to tell when data is loaded. With CFoundry, associated models were loaded as the object graph was traversed and the domain objects in Developer Console failed to encapsulate this behavior. This made it very easy to unwittingly write code that made excessive API requests.
2) It should be easy to tell where the data is coming from. CFoundry uses its domain modeling to infer the API endpoints to which it makes requests. This design makes it extremely easy to add new endpoints, but it makes it very difficult to reason about what particular endpoints will be called in a given situation. Since the Cloud Controller domain changes infrequently, we found that being able to understand what and how endpoints are called was provided more value to our team.
3) It should be easy to know what values each object provides. Domain models should be immutable and always instantiated with a full complement of data. Existing domain models in Developer Console lazily loaded data from Cloud Controller depending on what methods were called on it and what data it already had. This design made it difficult to predict what API calls an object in any particular state might make, which in turn made stubbing web requests for integration tests challenging.
4) It should be easy to provide fake data in tests. Data store access should exist in a layer separate from domain objects to permit simple faking of persisted data for feature specs.
The set of patterns we arrived at were heavily influenced by the idea of Domain Driven Design. In particular, we started by adding Entity, Aggregate, and Repository objects to the Developer Console code base. Our Repository objects encapsulate the retrieval of data from the various services we talk to and emit a fully hydrated and immutable domain object (either an Entity or an Aggregate).1 So far this new pattern has produced a number of positive benefits.
Our team members have found the Repository pattern makes it easier to understand how data is being retrieved. Cloud Controller data is returned as a simple hash by a concise restful client that is responsible for building requests, parsing JSON and error handling. The calls and endpoints necessary to create, retrieve, update and destroy each object are clearly enumerated in that domain object’s Repository. Consolidating all data access that occurs for a particular object gives us a single point where we can choose to optimize or cache the retrieval of data — for example, it is obvious which requests could be made in parallel in order to speed up the overall response.
The Repository pattern also improves our ability to write understandable and maintainable tests because it provides a discrete layer that can be stubbed out during testing. Where before we struggled to create web response stubs that consistently described a particular data set, it is fairly trivial to write a collection of fake Repositories that can maintain state. Further, Entities and Aggregates take only primitives and other Entities at instantiation time, making them easy to create in a test environment. Feature tests will eventually be as simple as telling the repositories about the domain objects they contain and then allowing the rest of the system to operate normally.
Repository objects, in turn, are test driven in an integration environment, meaning we don’t have to guess at what the responses will be, we simply spin up the test and look at the values that are returned by Cloud Controller. This approach also gives us the ability to run our repository integration suite against updated external dependencies to ensure that no regressions have occurred. Our previous approach to integration testing occurred at the GUI layer, necessitating a deploy to the environment in question before we could confirm that our code still worked correctly.
We’re excited that this new way of interacting with Cloud Controller will help us complete new features faster with fewer defects. I’ve posted a gist with some sample objects to help you better visualize our new approach; while this code is certainly not feature complete, we welcome feedback on how this pattern might work (or not) for your own interactions with Cloud Controller.
1 You might be wondering how we create or update these objects; these operations act on separate and similarly immutable objects whose responsibility is specifically for persistence, copying data from the retrieved object as necessary.