This repository contains a demo code sample intended to use the following concepts and technologies:
- API-first Development Approach
- Open API / Swagger
- Hexagonal (Ports & Adapters) Architecture
- Infrastructure as code (IaC)
- Docker
- Google Cloud Platform (GCP)
- Google Kubernetes Engine (GKE)
- Cloud Build
- Cloud Deploy
- Terraform
- Helm
- Skaffold
This project implements a todo service API, which lacks complex business logic. This implies that a simpler approach could be utilized since, fundamentally, the todo backend issue is a CRUD problem.
The todo service problem is utilized here as an illustrative example to demonstrate Hexagonal (Ports & Adapters) Architecture without the complexity of business logic overshadowing the architectural concepts being presented.
For real-life scenarios, it's important to note that Domain Driven Design (DDD) and Hexagonal (Ports & Adapters) Architecture are intended for problems with large and complex business logic. Applying these concepts to straightforward CRUD problems may lead to overengineering and undesired accidental complexity.
API for the service is described under api.yaml.
This project uses Hexagonal architecture, also known as Ports and Adapters architecture. Hexagonal architecture is a design pattern that emphasizes decoupling the core business logic of an application from its external dependencies. In this architecture, the core logic is surrounded by ports, that handle communication with external systems such as databases, user interfaces, or third-party services. The purpose of Hexagonal Architecture is to improve maintainability and testability by promoting separation of concerns and reducing the impact of changes in external dependencies on the core application logic.
Image Source: Hexagonal Architecture Explained - https://www.arhohuttunen.com/hexagonal-architecture/
By separating concerns and dependencies, Hexagonal Architecture enables meaningful business use case testing through the implementation of "ports" or interfaces. The ports define the contract between the core application logic and its external dependencies, allowing for easy substitution of implementations during testing. For example, in unit tests, mock implementations of these ports can be used to simulate interactions with external systems, enabling meaningful testing of business logic in isolation.
Image Source: Hexagonal Architecture Explained - https://www.arhohuttunen.com/hexagonal-architecture/
For more information on Hexagonal (Ports & Adapters) Architecture please see Hexagonal Architecture Explained .
This project uses the Detroit School of Testing to achieve meaningful tests for Domain Services exposed using Ports. This is made possible by adopting Hexagonal Architecture and separating Business concerns from Technical concerns.
Mocking (using for example Mockito) is limited to achieve meaningful testing. Fakes are used, for example (InMemoryTodoItemsRepository.java), to test behaviors in isolation.
Testing pyramid is preserved by limiting amount of end-to-end tests, and most tests are implemented in isolation as Domain Services tests.
Following types of tests are implemented (from top-to-bottom):
- Acceptance Tests
- API Tests
- Domain Services Tests
- Domain Model Tests
More on this subject can be found under:
Unit Testing - Principles, Practices, and Patterns by Vladimir Khorikov
Pragmatic Unit Testing by Vladimir Khorikov
API-first development approach is a methodology that prioritizes designing and building the application programming interface (API) before implementing other aspects of the software system. This strategy ensures that the API is well-defined, robust, and supports intended use cases. Open API, formerly known as Swagger, plays a crucial role in API-first development by providing a standardized format for documenting APIs. With Open API, developers can describe the functionality, data models, and endpoints of their APIs in a standardized format, allowing integration and collaboration across teams. API-first approach leverages tools like Open API and Swagger.
Project uses Cloud Build and Cloud Deploy for CI/CD Pipeline. Final artifact as Docker Image is stored in Artifact Registry and deployed to GKE.
CI pipeline is described in cloudbuild.yaml file. Cloud Build is created using Terraform under cloud_build.tf. Docker Images are kept in Artifact Registry created under docker_repository.tf. Additionally simplified version fo CI pipeline runs under GitHub Actions to execute tests only. GitHub Actions pipeline is defined under maven.yml. The reason for having also simplified version of CI pipeline is ability to observe tests states even after GCP CI environment is unprovisioned.
CD pipeline is described in cloud_deploy.tf and skaffold.yaml. Kubernetes objects are rendered using Helm and Helm Chart is available under workload folder.
UI code for this service is available under gcp-gke-hexagonal-todo-service-ui.
Project is build using Maven:
mvn clean install
Docker Image can be created using following commands from service
folder level:
docker buildx build -t todo-service .
Once gcloud cli is configured, you can also trigger build in cloud using below command:
gcloud builds submit
When running build in cloud, Docker Image will be stored in Artifact Registry.
Following IntelliJ Run Configurations are included:
- Application - Local - executes application locally with In Memory Todo Items Repository
- Application - Sandbox - executes application against resources in GCP snd env
- Unit and Integration Tests - executes unit and integration tests
- Acceptance Tests - executes acceptance tests using Firestore in GCP
- All Tests - executes all tests in project
java -Dspring.profiles.active=env-local -jar target/todo-service-*.jar
java -Dspring.profiles.active=env-snd -jar target/todo-service-*.jar
Since service accesses Firestore in GCP Cloud when running against Sandbox Environment you need to make sure that when executed, application has access to Application Default Credentials.
docker run --rm -e SPRING_PROFILES_ACTIVE=env-local -p 8080:8080 todo-service
Since service accesses Firestore in GCP Cloud when running against Sandbox Environment you need to make sure that when executed as Docker Container, application has access to Application Default Credentials.
Application Default Credentials will be stored
under ~/.config/gcloud/application_default_credentials.json
. Docker can access it using Docker volume.
Below are the commands that will setup Docker volume and credentials on volume:
docker volume create gcp_credentials
sudo mkdir /var/lib/docker/volumes/gcp_credentials/_data/gcloud
sudo cp ~/.config/gcloud/application_default_credentials.json /var/lib/docker/volumes/gcp_credentials/_data/gcloud
sudo chmod -R 0700 /var/lib/docker/volumes/gcp_credentials/_data/gcloud
sudo chown -R 1001:1001 /var/lib/docker/volumes/gcp_credentials/_data/gcloud
Having volume and credentials setup done, you can run container with Application Default Credentials available for the application inside the container.
docker run --rm -e GOOGLE_APPLICATION_CREDENTIALS=/opt/app/.config/gcloud/application_default_credentials.json -e SPRING_PROFILES_ACTIVE=env-snd -v gcp_credentials:/opt/app/.config -p 8080:8080 todo-service
Deployment on top of Google Kubernetes Engine (GKE) is done using Cloud Deploy. Cloud Build triggers release on last step of CI pipeline described under cloudbuild.yaml. Kubernetes files are rendered by Helm. Helm Chart is defined under workload. During deployment skaffold.yaml is responsible for triggering Helm when executing under Cloud Deploy.
curl -H 'Content-Type: application/json' \
-d '{ "name": "Buy groceries", "completed": false }' \
-X POST \
http://localhost:8080/todos
curl -X GET localhost:8080/todos
curl -X GET localhost:8080/todos/f3a1df6d-091d-4b9b-8005-4c463fb0792e
curl -H 'Content-Type: application/json' \
-d '{ "name": "Clean the garage", "completed": true }' \
-X PUT \
http://localhost:8080/todos/f3a1df6d-091d-4b9b-8005-4c463fb0792e
curl -X DELETE localhost:8080/todos/f3a1df6d-091d-4b9b-8005-4c463fb0792e
Hexagonal Architecture Explained. Accessed April 24, 2024. https://www.arhohuttunen.com/hexagonal-architecture/
Dominik Cebula