Inter-service communication

Service can use RPC (Remote procedure call) or Messaging communication style.

RPC communication style is based on request-reply protocol. It is simple to use, but it creates coupling between client service and provider service.

Messaging provides loose runtime coupling, but adds additional complexity to the system because you need to define message broker and message pool.

Remote procedure call (RPC)

Note

Silvera's RPC communication is generalization of Remote Method Invocation described here: https://microservices.io/patterns/communication-style/rpi.html

When using RPC, in order for two services to be able to communicate, you must define a dependency between them.

In the example that follows, we will define two dependencies for Order service. These are User service and Storage service. More precisely, Order service depends on User service methods userExist and userEmail, and Storage service method takeIngredient.

dependency Order -> User {
    userExists[fail_fast]
    userEmail[fail_fast]
}

dependency Order -> Storage {
    takeIngredient[fail_fast]
}

Based on the dependency declaration, Silvera will generate a code that can be used to interact with the target service.

What happens if User service or Storage service is unavailable when request takes place? Luckily, Silvera implements Circuit Breaker pattern, and how the situation is handled is defined by one of the following options:

  • fail_fast - exception will be raised in the client if API call fails (default behavior),
  • fail_silent - returns an empty response,
  • fallback_method - defines a method that will be called in case the original method fails,
  • fallback_static - returns default values, and
  • fallback_cache - returns a cached version of response if present, otherwise returns empty response like fail_silent.

Note

More information about the Circuit Breaker design pattern can be found here: https://microservices.io/patterns/reliability/circuit-breaker.html

Messaging

Messaging communication style depends on two things: message broker, and message pool.

Note

In the current version, Silvera supports only Kafka as a message broker.

Note

More information about the Messaging design pattern can be found here: https://microservices.io/patterns/communication-style/messaging.html

Message pool

Message pool is globally available object that contains all messages that are used withing the system. Message can contain parameters (command message) or not (event).

Here's how to define a message pool with two messages:

msg-pool {

    // define message group Commands
    group Commands [

        msg CreateCommand[
           str param1
           str param2
           ...
        ]
    ]

    // define message group Events
    group Events [
        msg StorageItemTaken[]
    ]
}

Message broker

Message broker is an entity whose responsibility is to deliver messages to a destination.

Message broker contains channels where messages are stored. Services that add messages to channels are called publishers, while services that read messages from channels are called consumers. A service can be both publisher and consumer at the same time.

Here's how to define a message broker:

// message broker is named Broker
msg-broker Broker {

    channel CMD_CREATE_COMMAND_CHANNEL(Commands.CreateCommand)
    channel EV_STORAE_ITEM_TAKEN_CHANNEL(Events.StorageItemTaken)

As shown in the example above, channels in Silvera are typed. Which means, only messages of certain type can be added to the channel.

How do I send messages?

In order to send a message, you need to annotate an API method with @producer annotation.

In following example, method takeFromStorage will send Events.StorageItemTaken message to the EV_STORAGE_ITEM_TAKEN_CHANNEL channel defined in broker named Broker.

service Order [

    ...

    api {
        ...
        @producer(Events.StorageItemTaken -> Broker.EV_STORAGE_ITEM_TAKEN_CHANNEL)
        bool takeFromStorage(i32 itemId)
    }
]

Note

Silvera will generate code that will publish the command to the messaging broker, but setting values to command's attributes need to be handled manually, otherwise empty message will be sent!

How do I receive messages?

In order to consume a message, you need to annotate an API method with @consumer annotation.

In following example, internal method itemTakenListener will receive Events.StorageItemTaken message from the EV_STORAGE_ITEM_TAKEN_CHANNEL channel defined in broker named Broker.

service StorageListener [

    ...

    api {
        ...

        internal {
            @consumer(Events.StorageItemTaken <- Broker.EV_STORAGE_ITEM_TAKEN_CHANNEL)
            void itemTakenListener()
        }

    }
]

In the generated code, itemTakenListener will have Events.StorageItemTaken object as a parameter.