Design patterns in Go: Bridge
Cristian Curteanu
Posted on January 25, 2021
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
}
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
Posted on January 25, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.