- Possibility to use different tech stacks and test environments in each separate service.
- When you implement significant changes in your service, you don't demand changes in other services. In general, one service team's changes won't break the entire app.
- Different repositories and development environments per service are allowed.
- Updates and bug fixes in monolithic apps demand taking all apps offline. Growing startups and big enterprises can't afford downtime.
- Due to significant progress in containerization technologies like, e.g., Kubernetes, microservices gives much bigger flexibility to split your app between specific hardware cores and memory areas and keep control over each service simultaneously. It makes life much easier for the DevOps team.
- At the beginning of development, this architecture demanded much more attention from the technical lead. Development team need to decomposed a code by the business needs. Select the correct type of communication between services, databases design. Plenty of technical decisions need to be taken before the lunch of the development process. It takes time.
- Architecture demands to keep separate teams for each service which is less cost-effective.
- With such a distributed system, you need to install centralized logs to bring everything together and keep that maintainable.
- Debugging via local IDE won't work. Systems, in most cases, are complex. Plenty of different processes communicate with each other asynchronously. To find a bug, you need to reproduce specific conditions of multiple various, unrelated services. Most of the software developers rely on sophisticated logging tracing systems like, e.g., Opentracing or Squash.
1. Available decomposition approaches:
- Decompose by business capability
- Decompose by subdomain
- Self-contained Service
- Service per team
We decomposed our service using decomposition by subdomain, which means that each subdomain corresponds to a different part of the business. We try to avoid splitting the business relation set of functions between other services and making our developers easy to understand and manage. This way, services are loosely coupled.
E.g., our architecture embraces the following list of microservices:
- Pages MS
- Domains and Users MS
- Products MS
- Orders MS
- Product Deliveries MS
- Payments MS
- Integrations and mapping MS
- SSR WebPage rendering MS
- Statistics MS
Definitely, in this approach, you need to understand your business very well to decouple architecture properly.
We thought about a Self-contained Service approach, but they are much harder to maintain in splitting business logic and responsibility. Same functions may be implemented in different services, which we want to avoid as much as possible.
An example of the structure of that service you can see in picture:
2. Available Data management approaches:
- Database per Service
- Shared database
- API Composition
- Domain event
- Event sourcing
We chose a database per service approach, but we have one service which has a shared database. It was decomposed from other MS and is not temporal because there were many data dependencies. Removing those dependencies would take too much time. So in the future, we are planning to do it, but we have more significant priorities at the moment.
In BOWWE, each MS has its own database, which is either MySQL, Cassandra, or Redis. We also use Redis as a distributed cache.
Such structures complicate situations when specific tasks demand access to two different databases owned by two other microservices. Sometimes tasks need to change multiple different databases to execute tasks. In BOWWE, we have had such a type of problem, especially in ‘Orders MS’, which requires communication between products, payments, and delivery microservices.
We considered the CQRS approach, but for this time, it was additional effort and cost for us, so we decided to leave it as an option in the future in case of performance issues.
An example of that databases structure you can see in picture above.
Service Component Test - we use the service Component Test by mocking external service calls. This approach is easier and cheaper. It doesn't require a cross-service test environment setup, but we can have some integration problems that our tests won't catch.
Consumer-driven Contract Test - we are using those kinds of tests to test integration with external services and ensure that their contract has not been changed. So, in that case, we are doing a Consumer-side contract test at the same time.
We are also using cypress for end-to-end and visual regression tests, which allows us to decrease the number of manual testing before each release and gives us better confidence that the functionality we release is working fine.
4. Available Deployment patterns:
- Multiple service instances per host
- Service instance per host
- Service instance per VM
- Service instance per Container
- Serverless deployment
- Service deployment platform
We are using the approach of having multiple services per host. Our services are running on dedicated servers. This is more cost-effective for us in comparison with hiring cloud infrastructure.
We are planning to run each service in a separate container. It will make our services more scalable and easier to configure.
An example of that approach you can see in picture:
5. Available Communication styles:
- Remote Procedure Invocation
- Domain-specific protocol
- Idempotent Consumer
For communication, we are using RestAPI and Apache Thrift. Apache Thrift is a framework with a cross-language code generator that helps us build faster communication between clients and services regardless of the used development languages. In BOWWE, Apache Thrift is used for communication between services responsible for getting page data. We choose Apache Thrift because of better latency compared to Rest API and programming language independence. Apache Thrift supports most of the languages, and our core services are written in Java and PHP. We also use REST APIs where latency is not so important and for our public API.
An example of that RestAPI communication structure you can see in picture:
6. Available Observability solutions:
- Log aggregation
- Application metrics
- Audit logging
- Distributed tracing
- Exception tracking
- Health check API
- Log deployments and changes
None of the above.
Each service instance generates its logs and writes to a log file in a standardized format. The log files contain errors, warnings, information, and debug messages. So far, this approach is good enough for us. In the future, we may start to use dedicated tools like, e.g., Splunk, Logstash, or Appdynamics.
Regarding observability, we can add that BOWWE possesses a dedicated microservice that delivers deep analytics about traffic and customer behavior to the owners of the websites. Tech stack of that microservice:
Spring Boot uses the NoSql Cassandra database. In the future, we are thinking about introducing Apache Spark.
From our experience, building microservices architecture from the very beginning of the project was an excellent decision. Definitely, in the beginning, we have spent a higher number of working hours on the architecture design of the solution, but later it was much easier to develop each functionality separately and manage a few development teams simultaneously. At this point in the project life, adding such significant parts of code like e-commerce creator and later in the future web app builder would be impossible without this architecture.
If you would like to ask about more details of that project, please don't hesitate to contact us via firstname.lastname@example.org email address.