12.1 Factor Apps: Dev/Prod Parity
Hello and Adam here to serve up the next episode of Small Batches in the 12.1 Factor App series. This episode covers the dev/prod parity factor. Let’s dive in.
The original 12 factor app guideline [¹] states that applications should use the same versions of backing services in development and in production. Recall that a “Backing Service [²]” is any service the app consumes over the network. Examples include datastores like MySQL or Redis and external APIs like Amazon S3. The original 12 factor app guidelines for the “dev/prod” and “backing services” factors mostly focus on datastores. This opens the dev/prod factor to interpretation regarding external APIs, which leads to wildly different outcomes depending on the interpretation.
If you take the original guidelines at face value, then your development environment will make network calls to the same versions of backing services. That assumes all backing services are in fact running and accessible. This is problematic for multiple reasons:
Consider a distributed system. Is it possible — ignoring whatever effort that requires — to run all services in a single development environment? If so, does your hardware have the necessary compute resources to support it? If not, then what’s the solution and how much parity dev/prod parity is there as a result?
Consider a third-party API like Twitter. Is the development environment going to make real tweets? If so, to which Twitter account? Does each development environment need a separate Twitter account? If so, does that change the utility of the development environment?
These examples show the added complexity in assuming that all backing services are available in the development environment. In the best case, it’s functional but fragile. In the worst case, it creates a mess of dependencies and saddles the team with toil[³]. In my view, These outcomes come from the assumption that development environments should be fully integrated environments masquerading under the banner of dev/prod parity.
The 12.1 factor app takes a different approach. The 12.1 factor app strives for dev/prod parity where practical and eschews it when not. This requires differentiating between bounded and unbounded contexts.
A typical service uses a datastore and interacts with external services. In this case, the datastore is within the bounded context. It’s assumed that service consumers will access the data via an API instead of accessing the datastore directly because doing so violates the bounded context. In this case, achieving dev/prod parity is practical and certainly useful. The service must be developed and tested against the same version used in production. Tools like Docker make it trivially easy to do, so there’s no argument to be made against it.
This typical service also interacts with external services. These are outside the bounded context because the service has no control over them but is still dependent on them. The 12.1 factor app eschews dev/prod parity for these backing services. These backing services should not be used in development or test. They should be replaced with mocks with tests and fakes in development. Mocks enable unit tests of interactions at the boundary. Fakes in development promote isolated environments and support forcing the consumer through behaviors which may not be possible in real environments.
Combining these two practices works well with a single service and scales up to multiple services. The 12.1 factor approach prefers locality [⁴] over fully integrated environments. Doing so promotes fast and independent iterations on discrete services using automated tests to verify correctness. End-to-End issues which may have been identified with a fully integrated environment with dev/prod parity should be pushed downstream in the deployment pipeline in accordance with test pyramid [⁵] principles. If a regression is identified, then it may quickly addressed by adding tests to the relevant service’s test suite.
These recommendations build on a substantial amount of prior knowledge like:
+ Bounded context from Domain Driven Design.
+ The Five Ideals in the Unicorn Project.
+ The role of team autonomy as discussed in Team Topologies.
+ The Hexagonal architecture used to swap between mocks, fakes, and real versions of external services.
+ The idea of growing production-ready software guided by tests.
Check the show notes for links on these topics. I hope this episode has given you some food for thought on the goal of dev/prod parity and its effects across the software development process.
That’s all for this one. Good luck out there and happy shipping.
References:
[¹]: https://12factor.net/dev-prod-parity
[²]: https://12factor.net/backing-services
[³]: https://landing.google.com/sre/sre-book/chapters/eliminating-toil/
[⁴]: https://www.infoq.com/articles/unicorn-project/ [⁵]: https://martinfowler.com/articles/practical-test-pyramid.html