Sobes.tech
Junior — Senior

Decorator with merging simultaneous client requests

livecode

Task condition

It is necessary to create a wrapper-decorator around the Client interface that will guarantee that when concurrent calls to the GetUser(name) method are made:

  • if the provided names match (for example, "Vasya"), the actual implementation Client.GetUser("Vasya") will be executed only once;
  • if the names differ ("Vasya" and "Oleg"), the requests are processed independently.

The Client.GetUser method is resource-intensive and costly, so it is important that requests with the same parameter are executed only once, and the obtained result is shared among all goroutines making the request.

type User struct {
    Id int64
}

type IClient interface {
    GetUser(name string) (*User, error)
}

type Client struct {}

func (c Client) GetUser(name string) (*User, error) {
    // resource-intensive operation
    return &User{}, nil
}

func NewClient() *Client {
    return &Client{}
}

type Decorator struct {
    client IClient
    // ...
}

func (d *Decorator) GetUser(name string) (*User, error) {
    // ...
}

func NewDecorator(c IClient) *Decorator {
    return &Decorator{client: c}
}

Example usage:

func main() {
    client := NewClient()
    decoratedClient := NewDecorator(client)

    go func() {
        vasya, err := decoratedClient.GetUser("Vasya")
        // ...
    }()
    go func() {
        vasya, err := decoratedClient.GetUser("Vasya")
        // ...
    }()
    go func() {
        vasya, err := decoratedClient.GetUser("Vasya")
        // ...
    }()
    go func() {
        vasya, err := decoratedClient.GetUser("Vasya")
        // ...
    }()
    go func() {
        oleg, err := decoratedClient.GetUser("Oleg")
        // ...
    }()
    go func() {
        oleg, err := decoratedClient.GetUser("Oleg")
        // ...
    }()
}

The task requires implementing request synchronization, caching the result during execution, and correctly distributing errors/results among all callers.