Design patterns in Go: Bridge

01 Jan 2021

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
}

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
}

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
}

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
}

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")

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

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