Skip to content

Development Guidelines

Splitting the monolith's components out into services you have a choice when building each of them. You want to use Node.js to standup a simple reports page? Go for it. Python for a particularly gnarly near-real-time component? Fine. You want to swap in a different flavour of database that better suits the read behaviour of one component? We have the technology to rebuild it. Of course, just because you CAN do something, doesn't mean you SHOULD - but partitioning your system in this way means you have the option.

Development Languages & methodology

12 factor App Methodology

The Twelve Factor Apps:

A modern methodology for building software-as-a-service apps

Every microservice development MUST adhere to the 12 factor apps methodology.

Microservices development MUST comply to each one of the 12 factors defined in the methodology

Polyglot Microservices

Remember this quote when you are choosing a framework:

"It's not the arrow, but the archer"

Microservices MUST be developed using: Python, PHP or Javascript. Any other language development MUST be validated first.

It is RECOMMENDED to use language specific microframeworks to speed-up development such as:

It is NOT RECOMMENDED to use a all-inclusive framework (Laravel, Django, SailsJS, etc) to develop microservices. They add unnecessary fat to a project that usually translates in code complexity. Remember the YAGNI principle.

Testing Approach & Automated testing

Microservices SHOULD be implemented using automated testing.

It IS RECOMMENDED to adhere to Test Driven Design (TDD) practices.

It is RECOMMENDED to adhere to a Tests First approach.

Logging

Logging best practices

A great reference of the 13 best practices you should know when logging.

Microservices MUST treat logs as an event stream. You MUST NOT create log files.

English MUST be used as the default language in logs.

Each Microservice MUST write its event stream to stdout. This allows the logs to be aggregated in production environment and be treated as a single event stream.

In Development, the programmer SHOULD view this stream in the foreground of the terminal to observe the app’s behavior (if needed).

It is RECOMMENDED to use open-source libraries to stream logs and don't reinvent the wheel.

You SHOULD always log at the proper level.

You MUST NOT log sensitive information under any circumstances.

A Microservice MUST NOT log at a debug-level in a production environment.

Events Publishing and Consumption.

Each Microservice MUST declare and configure (via code) its own queue to publish and receive events. Remember the "dumb pipes, smart endpoints" principle.

The event consumption MUST be handled by an separate process (usually a CLI command) running on the background.

It is RECOMMENDED that the event consumption is an infinite respawning process. This allows the service to handle any infrastructure issue and alert accordingly.

Processors

It is RECOMMENDED to bind a unique processor for each type of event the microservice consumes. Usually frameworks allow this type of configuration via a Service Provider Pattern or similar.

Examples

mezzio/mezzio src/App/ConfigProvider.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

namespace App;

class ConfigProvider
{
    ...

    /**
     * Get the (Integration) Events Processors (Handlers)
     *
     * @return array
     */
    public function getIntegrationEventsProcessors() : array
    {
        return [
            'sales.notifications.email_message_delivery_failed' => 
            \App\Email\Processor\MessageDeliveryFailedEventProcessor::class,

            'sales.notifications.email_message_sent' => 
            \App\Email\Processor\MessageSentEventProcessor::class,

            'sales.notifications.email_message_bounced' => 
            \App\Email\Processor\MessageBouncedEventProcessor::class,

            'sales.clients.applicant_created' => 
            \App\Email\Processor\ApplicantCreatedEventProcessor::class,
        ];
    }
}
app/queue/event_type_delegate_processor.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from app.queue.event_processor import EventProcessor
from app.queue.processors.applicant_created_event_processor import ApplicantCreatedEventProcessor
from app.queue.processors.applicant_deleted_event_processor import ApplicantDeletedEventProcessor
from app.queue.processors.applicant_restored_event_processor import ApplicantRestoredEventProcessor
from app.queue.processors.applicant_updated_event_processor import ApplicantUpdatedEventProcessor
from aio_pika.abc import AbstractIncomingMessage
from kink import di
import asyncio


class EventTypeDelegateProcessor(EventProcessor):

    def __init__(self):
        self.__processors_registry = {
            'sales.applicants-management.applicant_created': lambda: di[ApplicantCreatedEventProcessor],
            'sales.applicants-management.applicant_updated': lambda: di[ApplicantUpdatedEventProcessor],
            'sales.applicants-management.applicant_deleted': lambda: di[ApplicantDeletedEventProcessor],
            'sales.applicants-management.applicant_restored': lambda: di[ApplicantRestoredEventProcessor],
        }

    async def process(self, message: AbstractIncomingMessage) -> None:
        event_type = message.type

        print(f' [x] Received message {event_type}')

        if not event_type:
            print('No event type was provided.')
            return await message.reject()

        processor = self.__processors_registry.get(event_type)

        if processor is None:
            print(f'No processor found for event type {event_type}.')
            return await message.reject()

        async with message.process():
            # Ack is automatically handled, unless ignore_processed is true.
            # When an exception is raised, the message will be rejected.
            await processor().process(message)

            await asyncio.sleep(1)

If a microservice gets a message delivery of a type it cannot handle, it is RECOMMENDED to log such events to make troubleshooting easier.

The same way; If a consumer gets a delivery of a payload with errors, it is RECOMMENDED to log this data and log/alert accordingly.

Remember:

"Design for failure, and nothing will fail"

The processors MUST return an ACK or a REJECT depending of the delivery they receive. Generally client libraries can handle this automatically.