Design patterns in Go: Bridge

cristicurteanu

Cristian Curteanu

Posted on January 25, 2021

Design patterns in Go: Bridge

This article was originally published here

Have you ever had the situation when you need to use different database drivers for your repository. Well, in order to avoid using separate repositories for different database drivers, I would suggest to use Bridge design pattern to make things more elegant.

Problem

Let's say that you have started to create an application, and you designed it to use MySQL database. It's actually a good choice to start with, essentially it is always good to start with a relational database. But you came across an issue: your start receiving errors alerts in the logs like database connection pool exceeded. The problem is that a MySQL database is not as easy to scale horizontally as a NoSQL database, like MongoDB.

So you decide to migrate to a MongoDB database. You already prepared the infrastracture, but the problem now is that you have to migrate all data, but still keep it consistent, at least before the moment when you will prepare a migration script for that.

In order to make transition easier, you need to be able to connect with both databases. Hence, you will need to use a bridge, to migrate all the data.

Let's take a look on how an implementation would look like.

Solution

First of all, we will need an interface with common driver operation (CRUD). For this current implementation example we will use on Create and Read actions, for the sake of simplicity:

type Database interface {
    // For now only two methods will be defined, just illustrate the idea
    Create(input map[string]interface{}, result interface{}) error
    Find(id string, result interface{}) error
}
Enter fullscreen mode Exit fullscreen mode

Let's see how the MySQL driver will look like, in the context of this new interface:

type MySqlConnection struct {
    url string
}

func NewMySqlConnection(url string) *MySqlConnection {
    return &MySqlConnection{url}
}

type MysqlDatabase struct {
    conn *MySqlConnection
}

func NewMysqlDatabase(conn *MySqlConnection) *MysqlDatabase {
    return &MysqlDatabase{conn}
}

func (ms MysqlDatabase) Create(input map[string]interface{}, result interface{}) error {
    fmt.Printf("Creating %s using MySQL\n", reflect.TypeOf(result))
    return nil
}

func (ms MysqlDatabase) Find(id string, result interface{}) error {
    fmt.Printf("Fetching %s, with id %s, using MySQL\n", reflect.TypeOf(result), id)
    return nil
}

Enter fullscreen mode Exit fullscreen mode

I have also added, some rudimentary MySQL connection object, that should be injected into driver, but is can have a different structure for a production system.

Now let's add a repository and model struct, in order to operate somehow; let's say it will be User model:

type User struct {
    FirstName string
    LastName  string
    Email     string
}

type UserRepository struct {
    db Database
}

func NewUserRepository(db Database) *UserRepository {
    return &UserRepository{db}
}

func (repo UserRepository) Create(user User) error {
    data := map[string]interface{}{
        "first_name": user.FirstName,
        "last_name":  user.LastName,
        "email":      user.Email,
    }

    err := repo.db.Create(data, &user)

    return err
}

func (repo UserRepository) Find(id string) (User, error) {
    var result User
    var err error

    err = repo.db.Find(id, &result)

    return result, err
}
Enter fullscreen mode Exit fullscreen mode

As we can see here, it does a preprocessing for User data that will be used for drivers in order to create new records. And also the result will be written to the reference of the User that is passed to drivers.

In order to add the MongoDB driver, we will simply implement the Database interface for MondoDB driver:

type MongodbConnection struct {
    url string
}

func NewMongodbConnection(url string) *MongodbConnection {
    return &MongodbConnection{url}
}

type MongoDatabase struct {
    conn *MongodbConnection
}

func NewMongoDatabase(conn *MongodbConnection) *MongoDatabase {
    return &MongoDatabase{}
}

func (md MongoDatabase) Create(input map[string]interface{}, result interface{}) error {
    fmt.Printf("Creating %s using MongoDB\n", reflect.TypeOf(result))
    return nil
}

func (md MongoDatabase) Find(id string, result interface{}) error {
    fmt.Printf("Fetching %s, with id %s, using MongoDB\n", reflect.TypeOf(result), id)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

And now let's test it:

    var db Database

  if os.Getenv("DB_DRIVER") == "MY_SQL" {
        db = NewMysqlDatabase(NewMySqlConnection(os.Getenv("DB_URL")))
  } else if os.Getenv("DB_DRIVER") == "MONGO_DB" {
    db = NewMongoDatabase(NewMongodbConnection(os.Getenv("DB_URL")))
  } else {
    panic("Unknown DB driver had been given")
  }

    repo := NewUserRepository(db)

    repo.Create(User{
        FirstName: "John",
        LastName:  "Smith",
        Email:     "john.smith@gmail.com",
    })

    repo.Find("1")
Enter fullscreen mode Exit fullscreen mode

which, if the DB_DRIVER will have MONGO_DB value, it will produce following:

Creating *main.User using MongoDB
Fetching *main.User, with id 1, using MongoDB
Enter fullscreen mode Exit fullscreen mode

Conclusion

The bridge as we saw, separates the implementation from the interface, which can be used in the client object, only by injecting with specific implementation. This could be very handy, in other scenarios, when you need to have several implementations of the same abstraction, and have a client that will just consume the injected object only by using it's abstraction, which as a result might help when implementing an Abstract Factory

💖 💪 🙅 🚩
cristicurteanu
Cristian Curteanu

Posted on January 25, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Design patterns in Go: Bridge
go Design patterns in Go: Bridge

January 25, 2021