DEV Community

Cover image for Golang Design Patterns - Proxy
Le Xuan Kha (Leo)
Le Xuan Kha (Leo)

Posted on

Golang Design Patterns - Proxy

I. Proxy - Structural Pattern

The Proxy is a familiar Design Pattern for developers, used to represent an original resource. The Proxy provides functionalities similar to the original resource but with high customizability.

The Proxy acts as a substitute for the original resource, hiding complex details or adding necessary features such as access control, caching, or load balancing.

II. Real-world Example

The system frequently needs to query user information from a main database. However, directly querying the main database can increase latency and reduce system performance. Therefore, we use a Proxy that acts as a cache to temporarily store frequently queried data.

The Proxy checks the cache, and if the data exists in the cache, it returns it immediately. Otherwise, it queries the main database and updates the cache.

This use case introduces the mechanism of a Proxy for searching users (User) between the MainDB (main database) and the Stack (cache) to improve performance.

  1. Definition of User:
    A User only has an ID attribute (int), representing a user entity.

  2. Shared Interface:
    The UserFinder interface defines the Find(ID int) method to search for a user by their ID. Both MainDB and Stack adhere to this interface.

  3. Main Database (MainDB):
    The main database where users are stored. UsersDB provides the Find function to search for a user and the Add function to add a new user.

  4. Proxy (UserFinderProxy):
    The Proxy is a middle layer that uses the Stack (cache memory) for faster access before searching in the MainDB.

  • When a User is not in the Stack, the Proxy checks the MainDB.
  • If the user is found, the Proxy saves the User in the Stack to optimize subsequent queries.
  • The Stack has a capacity-limiting mechanism and auto-updates when full.

The class diagram for the above scenario would be depicted as follows:

Proxy Class Diagram

III. Implementation

  • Definition of the Interface User entity:
package proxy

type User struct {
    ID int
}
Enter fullscreen mode Exit fullscreen mode
  • Definition of the Interface:
package proxy

type UserFinder interface {
    Find(ID int) (User, error)
}
Enter fullscreen mode Exit fullscreen mode
  • Definition of Database
package proxy

import "fmt"

type UsersDB []User

func (u *UsersDB) Find(ID int) (User, error) {
    for _, user := range *u {
        if user.ID == ID {
            return user, nil
        }
    }
    return User{}, fmt.Errorf("user not found")
}

func (u *UsersDB) Add(user User) *UsersDB {
    fmt.Println("Adding to database: ", user)
    *u = append(*u, user)
    return u
}
Enter fullscreen mode Exit fullscreen mode
  • Definition of Proxy:
package proxy

import "fmt"

type UsersStack []User

func (u *UsersStack) Find(ID int) (User, error) {
    for _, user := range *u {
        if user.ID == ID {
            return user, nil
        }
    }
    return User{}, fmt.Errorf("user not found")
}

func (u *UsersStack) Add(user User) *UsersStack {
    *u = append(*u, user)
    return u
}

type UserFinderProxy struct {
    MainDB   UsersDB
    Stack    UsersStack
    Capacity int
}

func (u *UserFinderProxy) Find(ID int) (User, error) {
    user, err := u.Stack.Find(ID)
    if err == nil {
        fmt.Println("Found in stack: ", user)
        return user, nil
    }

    user, err = u.MainDB.Find(ID)
    if err != nil {
        return User{}, err
    }

    fmt.Println("Found in mainDB: ", user)
    u.AddToStack(user)
    return user, nil
}

func (u *UserFinderProxy) AddToStack(user User) error {
    fmt.Println("Adding to stack: ", user)
    if len(u.Stack) >= u.Capacity {
        u.Stack = append(u.Stack[1:], user)
    } else {
        u.Stack.Add(user)
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode
  • Run the application:
/*
        Example Proxy
    */
    fmt.Println("*** Example Proxy ***")

    mainDB := proxy.UsersDB{}

    user1 := proxy.User{ID: 1}
    user2 := proxy.User{ID: 2}
    user3 := proxy.User{ID: 3}

    mainDB.Add(user1).Add(user2).Add(user3)

    proxy := proxy.UserFinderProxy{
        MainDB:   mainDB,
        Stack:    proxy.UsersStack{},
        Capacity: 2,
    }

    proxy.Find(1)
    proxy.Find(2)
    proxy.Find(3)
    proxy.Find(2)
    proxy.Find(1)

    fmt.Print("*** End of Proxy ***\n\n\n")
Enter fullscreen mode Exit fullscreen mode

With the output:

Output

IV. Explain the example above

This example illustrates how the Proxy pattern works through querying users (User) from the main database (MainDB) while using a stack-based cache (Stack) to optimize repeated search operations. Below is the step-by-step explanation:


1. Initializing the Main Database (MainDB)

mainDB := proxy.UsersDB{}

user1 := proxy.User{ID: 1}
user2 := proxy.User{ID: 2}
user3 := proxy.User{ID: 3}

mainDB.Add(user1).Add(user2).Add(user3)
Enter fullscreen mode Exit fullscreen mode
  • A main database (MainDB) is initialized, which holds a list of users (UsersDB).
  • Three users with IDs 1, 2, and 3 are added to the MainDB. Any query will be able to find these users if searched directly in MainDB.

Log results:

Adding to database:  {1}
Adding to database:  {2}
Adding to database:  {3}
Enter fullscreen mode Exit fullscreen mode

2. Initializing the Proxy with MainDB and Stack

proxy := proxy.UserFinderProxy{
    MainDB:   mainDB,
    Stack:    proxy.UsersStack{},
    Capacity: 2,
}
Enter fullscreen mode Exit fullscreen mode

The UserFinderProxy is created with the following components:

  • MainDB: The primary database that stores all the users.
  • Stack: A cache layer that is initially empty.
  • Capacity: The maximum number of users that the Stack can hold (set to 2 here).

3. Searching for Users

First Search: proxy.Find(1)

  1. Proxy checks for ID 1 in the Stack but finds nothing (Stack is initially empty).
  2. Proxy switches to searching in the MainDB and finds the user with ID 1.
  3. The proxy adds this user to the Stack for faster future queries.

Log results:

Found in mainDB:  {1}
Adding to stack:  {1}
Enter fullscreen mode Exit fullscreen mode

Second Search: proxy.Find(2)

  1. Proxy checks for ID 2 in the Stack but does not find it.
  2. Proxy searches the MainDB, finds the user with ID 2, and adds it to the Stack.
  3. The Stack now contains two users: {1, 2}.

Log results:

Found in mainDB:  {2}
Adding to stack:  {2}
Enter fullscreen mode Exit fullscreen mode

Third Search: proxy.Find(3)

  1. Proxy checks for ID 3 in the Stack but does not find it.
  2. Proxy searches the MainDB and finds the user with ID 3.
  3. The Stack, already at full capacity (2), operates on a FIFO (First In, First Out) mechanism:
    • It removes the oldest user (User with ID 1).
    • Then, it adds the new user (3).
  4. The Stack now contains {2, 3}.

Log results:

Found in mainDB:  {3}
Adding to stack:  {3}
Enter fullscreen mode Exit fullscreen mode

Fourth Search: proxy.Find(2)

  1. Proxy checks for ID 2 in the Stack and finds it directly.
  2. No need to query the MainDB—the search is instant.

Log results:

Found in stack:  {2}
Enter fullscreen mode Exit fullscreen mode

Fifth Search: proxy.Find(1)

  1. Proxy checks for ID 1 in the Stack but does not find it (it was removed earlier).
  2. Proxy searches the MainDB and finds the user with ID 1.
  3. The Stack, at capacity, removes the oldest user (User with ID 3) and adds the new user (1).
  4. The Stack now contains {2, 1}.

Log results:

Found in mainDB:  {1}
Adding to stack:  {1}
Enter fullscreen mode Exit fullscreen mode

4. End of Example

Final State:

  • Stack: Contains {2, 1} (the two most recently queried users).
  • MainDB: Remains unchanged, containing all users {1, 2, 3}.

Final Log:

*** End of Proxy ***
Enter fullscreen mode Exit fullscreen mode

Summary

  • For each query, the Proxy first checks the Stack to quickly locate the User.
  • If the User is not found in the Stack, the Proxy searches the MainDB and adds the result to the Stack for optimized future searches.
  • The Stack has a capacity limit, and when it is exceeded, the oldest User is removed (FIFO – First In, First Out).

Advantages of this approach:

  • Reduces the number of accesses to the MainDB when Users are queried repeatedly.
  • Improves system performance by utilizing the cache (Stack) for faster lookups.

V. Conclusion

We do not always need to use the Proxy design pattern to address similar problems. In some cases, direct access to the database might suffice if the problem is not complex or performance optimization is not required. In other situations, you might consider using the Decorator pattern or other optimization techniques.

The most important thing is to choose the solution that best fits your specific problem.

Thank you for taking the time to read this article! 😊

VI. References

  • Go Design Patterns (Mario Castro Contreras)
  • Full source code for Go design patterns: available here.

Top comments (0)