10 Workspace Guidelines for a Superior Developer Experience

The following guidelines are fairly opinionated and are based on the experience of the authors of this book. We recommend that you use these guidelines as a starting point.

After you have had experience building services with our guidelines, we expect that you may consider modifying some of them to better fit your individual needs and experiences:

1. Make Docker the only dependency.

2. Remote or local should not matter.

3. Ensure a heterogeneous-ready workspace.

Now, for the record: this does not in any way mean that in a well-managed microservices environment you should see every team picking whatever language and databases they feel like and going for it. Quite the opposite: when uncertain, definitely try to exercise caution and go with two, at most three, stacks. The point here is that you should be able to introduce a new stack if you genuinely needed it, so in your example setup, you have to show that you actually can, that is, by implementing more than one stack.

The Rule of Twos

We have found proactively practicing heterogeneity in a microservices setup to be a great approach. For any critical component in your system, make sure that you are using at least two alternatives in production at the same time — even when you only need one. You should also make sure that you have an infrastructure to support the two alternatives as easily as you would use a single one. We call this approach the “Rule of Twos”.

Say that most of your APIs are written in Node.js — a truly wonderful, I/O optimized stack for writing APIs. See if some of them could be implemented in Go, Java, Rust, etc., maybe because they do something more CPU-bound, which Node is not great at. While you practice heterogeneity, however, do make sure that you limit the selection of your programming languages and database systems across the entire application to two or three. Otherwise, you can run a high risk of confusing your teams with too much choice and creating serious maintenance overheads.

4. Running a single microservice and/or a subsystem of several ones should be equally easy

5. Run databases locally, if possible.

6. Implement containerization guidelines.

a. Even though the code runtime is containerized, developers must be able to edit code on a host machine (e.g., their laptop, an EC2 dev server), with any code editor. However, during execution, a full run/test/debug should be executed in a container.

b. Since Docker Compose can generally do anything a Dockerfile can, they can easily be confused by developers. As such, it is important to establish the difference between the two. We recommend the following formula: Use a Dockerfile for building a container image, and Docker Compose for running things locally, including complex integrations. An image built with a Dockerfile should be directly runnable on Kubernetes, AWS ECR, Swarm, or any other production-grade runtime. Please note that just because it can be doesn’t mean the local/dev image will always necessarily be the same as the one running in production. Teams do often optimize the former for usability and the latter for security and performance. A good example of this approach is the usage of multistage builds.

c. Multistage builds must be utilized in Dockerfiles to accommodate usage of slim images in production and usage of more full-featured images for local development.

d. Developer user experience is critical. Implementing hot-reloading of the code and/or the ability to connect a debugger out of the box is an important feature.

7. Establish rules for painless database migrations.

a. Any and all changes to a database schema must be codified in a series of “database migration” scripts. Migration files should be named and ordered by date.

b. Database migrations should support both schema changes as well as sample data insertion.

c. Running database migrations should be part of the project launch (via Make a start, see the next section) and must be enforced.

d. Running database migrations must be automated and should be part of any build (integration, feature branch builds for PR, etc.).

e. It should be possible to indicate which migrations run on which environments (or which ones can be skipped), so that migrations that deal with sample data creation can be skipped in production, for example.

f. These rules apply to all data storage systems: relational, columnar, NoSQL, and so forth.

g. Some examples:

a. Flyway hosts this introduction to database migrations

b. See this blog post by Daniel Miranda et al. about database migrations for Cassandra

c. Check out this example of using Node’s DB-migrate-SQL for a MySQL database

8. Determine a pragmatic automated testing practice.

a. Test-first, test-as-you-code, or test-after-code should all be acceptable practices as long as all code is covered with a reasonable amount of meaningful tests before it is merged with the main branch.

b. Teams should use a testing approach and frameworks that are idiomatic for the platform/stack in which code is being developed (e.g., JUnit for Java). The codebase of the same stack (e.g., Go, Java, etc.) should use a uniform approach and various microservices in the same language should not be doing different things based on who wrote them and when.

c. Using external tools, especially for acceptance or performance testing, is fine with proper justification, given an important caveat: these tools (e.g., Cucumber) must be fully integrated into the code/repository of the service itself, and using and running them must be as easy as a native solution. An average developer of the service should not need to set anything up to get things going and should be able to easily run tests with a command like make test-all.

d. Special attention and care should be given to automated tests that span the boundaries of individual microservices. They will have to be applied either at a higher level (e.g., an API that invokes microservices, or a UI), or in some cases, a dedicated repository may need to be set up to house testing orchestration and automation for such tests.

e. Code linting/static analysis tooling should be set up and a consistent configuration for the linter must be adapted for the organization’s style.

9. Branching and merging.

a. All development should happen on feature and bug branches.

b. Merging of a branch to the main branch should not be allowed without all tests (including integration tests in a temporary integration cluster spun up for the branch) passing on that branch.

c. The status of the test runs (after each commit/push) must be readily visible for code reviewers during pull requests.

d. Linting/static analysis errors should prevent code from being pushed to a branch, and/or merged into the main branch.

10. Common targets should be codified in a makefile.

We recommend defining and implementing the following standard targets for your microservice makefiles:

  • start: Run the code.
  • stop: Stop the code.
  • build: Build the code (typically a container image).
  • clean: Clean all caches and run from scratch.
  • add-module
  • remove-module
  • dependencies: Ensure all modules declared in dependency management are installed.
  • test: Run all tests and produce a coverage report. • tests-unit: Run only unit tests.
  • tests-at: Run only acceptance tests.
  • lint: Run a linter to ensure conformance of coding style with defined standards.
  • migrate : Run database migrations.
  • add-migration: Create a new database migration.
  • logs: Show logs (from within the container).
  • exec: Execute a custom command inside the code’s container.

This was part of my knowledge of reading the Book “Microservices up and Running.”

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store