To keep consistency along all our services, we define our main guidelines so everybody can collaborate.
Take consideration that every line of code written must be following our guidelines.
The requirement level keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" used in this document are to be interpreted as described in RFC 2119.
In this document, such keywords are highlighted using bold font.
At the code layer, we should ensure our flexibility and scalability too. We accomplish this with help of a robust architecture. Either Domain-Driven Design (DDD) by Eric Evans or unclebob’s Clean Architecture shall be used to write new services.
Leveraged from the Hexagonal Architecture, the three main concepts that define our business logic are Entities, Repositories, and Interactors.
- Entities are the domain objects (e.g., a Movie or a Shooting Location) — they have no knowledge of where they’re stored (unlike Active Record in Ruby on Rails or the Java Persistence API).
- Repositories are the interfaces to getting entities as well as creating and changing them. They keep a list of methods that are used to communicate with data sources and return a single entity or a list of entities. (e.g. UserRepository)
- Interactors are classes that orchestrate and perform domain actions — think of Service Objects or Use Case Objects. They implement complex business rules and validation logic specific to a domain action (e.g., onboarding a production)
With these three main types of objects, we are able to define business logic without any knowledge or care where the data is kept and how business logic is triggered. Outside of the business logic are the Data Sources and the Transport Layer:
- Data Sources are adapters to different storage implementations. A data source might be an adapter to a SQL database (an Active Record class in Rails or JPA in Java), an elastic search adapter, REST API, or even an adapter to something simple such as a CSV file or a Hash. A data source implements methods defined on the repository and stores the implementation of fetching and pushing the data.
- Transport Layer can trigger an interactor to perform business logic. We treat it as an input for our system. The most common transport layer for microservices is the HTTP API Layer and a set of controllers that handle requests. By having business logic extracted into interactors, we are not coupled to a particular transport layer or controller implementation. Interactors can be triggered not only by a controller, but also by an event, a cron job, or from the command line.
From Netflix Engineering, click here.
Therefore, we use our own API architecture implementing a Proxy pattern to hide our transport services implementation (like HTTP).
The following application architecture should be used whenever a new service is created.
We expect to keep consistency in error handling.
In the following section, we show our error handling scenarios by layer.
Domain: Business rule(s) validations
Type | Description | HTTP Status Code | Return value |
---|---|---|---|
InvalidID | Invalid identifier | 400 | Exception |
RequiredField | Missing required request field x | 400 | Exception |
InvalidFieldFormat | Request field x has an invalid format, expect value | 400 | Exception |
InvalidFieldRange | Request field x is out of range [x, y) |
400 | Exception |
Repository: Data source(s) validations
Type | Description | HTTP Status Code | Return value |
---|---|---|---|
EmptyRow | Resource(s) not found | 404 | Null/Nil |
Infrastructure | SQL/Docstore/API internal error | 500 | Exception |
Interactor: Domain's cases validation
Type | Description | HTTP Status Code | Return value |
---|---|---|---|
InvalidID | Invalid identifier | 400 | Exception |
RequiredField | Missing required request field x | 400 | Exception |
InvalidFieldFormat | Request field x has an invalid format, expect value | 400 | Exception |
InvalidFieldRange | Request field x is out of range [x, y) |
400 | Exception |
AlreadyExists | Resource already exists | 409 | Exception |
EmptyBody | Request body is empty | 400 | Exception |
In the following section, we define our logging official sentences.
Every logger must write in lowercase. (e.g. http service created)
Every logger must specify its layer location separating words with a dot. (e.g. service.transport)
Therefore, every logger shall use the specified sentences.
- New instance.- “HANDLER_NAME created”, “LAYER_LOCATION” (e.g. "media handler created", "service.transport.handler")
- New service.- “SERVICE_NAME started”, “service.LAYER_LOCATION” (e.g. "http proxy service started", "service.transport")
In the following section, we define our runtine configuration guideline.
Every configuration must define default values inside code.
Every configuration must have an "alexandria-config.yaml" file containing required keys, it must be stored in the following locations:
- $HOME/.alexandria/
- ./config/
- /etc/alexandria/
- .
Every configuration system must fetch secrets from AWS KMS or similar. If not available, read configuration from "alexandria-config.yaml" file.
Configuration file example
alexandria:
info:
service: "media"
version: 1.0.0
persistence:
dbms:
url: "postgres://postgres:root@postgres:5432/alexandria_media?sslmode=disable"
driver: "postgres"
user: "postgres"
password: "root"
host: "postgres"
port: 5432
database: "alexandria_media"
mem:
network: ""
host: "redis"
port: 6379
password: ""
database: 0
service:
transport:
http:
host: "0.0.0.0"
port: 8080
rpc:
host: "0.0.0.0"
port: 31337
For every single release, all software deployed must use Semantic Versioning guidelines to keep consistency and inform every single developer the best way possible.