logo
  • Core Gateway
  • Pricing
  • Install Convoy
  • Install Convoy
Blog
Product Update

Data Migrations in MongoDB using Golang

3 min read September 02, 2022

Written by

Subomi Oluwalana
Subomi Oluwalana

Co-Founder & CEO

Share

Hi there 👋🏽

Introduction

One of the reasons we decided to use MongoDB as the choice database for Convoy was the schemaless nature of webhook events. Events for various providers and use cases come in different shapes and sizes, and we wanted to support them all. The second reason we chose it, which is a simple corollary to the first, is it provides better tools to query JSON payloads; because JSON is more like the de-facto format for webhook events. We wanted to power to filter JSON efficiently. And lastly, it’s the NoSQL database we are more comfortable with.

While this has worked well for us, one requirement we did not anticipate was migrations. In building an OSS project, we need to provide an effective way for users to upgrade from lower to more recent versions. This includes migrating over their old data efficiently. In this article, we’ll talk about the problem we faced along these lines, the possible solutions that exist and the approach we went ahead with and will close out by describing possible future work.

The Problem & Possible Solutions

When upgrading software services, asides from providing a Changelog to users, explaining what’s new and what’s a breaking change, where possible, users should be able to run a command to easily upgrade to the latest software version bringing over their old data. If you build a project in rails, rails migrate solves this problem. This is similar to the technique used by Posthog; it was built with Python Django so running python manage.py migrate works!

But how do you solve this same problem easily with Golang and MongoDB. There are some solutions but these solutions don’t work if you’re building with Golang and MongoDB and especially if you need reproducible upgrades and downgrades. Let’s look through possible solutions and their drawbacks.

  1. golang-migrate: This solution is like the de-facto migration tool for Go. It’s really great, because it supports a variety of databases, even MongoDB. Its MongoDB driver uses JSON files to describe up and down migrations. See below for an example:

    [
    	{
    		"aggregate": "users",
    		"pipeline": [
    			{
    				"$project": {
    					"_id": 1,
    					"firstname": 1,
    					"lastname": 1,
    					"username": 1,
    					"password": 1,
    					"email": 1,
    					"active": 1,
    					"fullname": { "$concat": ["$firstname", " ", "$lastname"] }
    				}
    			},
    			{
    				"$out": "users"
    			}
    		],
    		"cursor": {}
    	}
    ]
    

    The problem here is it requires you to learn a lot of MongoDB queries to perform basic operations. Compare this to a similar solution in rails:

    class AddFullNameToUsers < ActiveRecord::Migration[6.1]
    	def change
    		add_column :users, :fullname, null: true
    
    			User.each do |user|
    				user.update!(fullname: user.firstname + " " + user.lastname)
    			end
    
    		change_column_null :users, :fullname, false
    	end
    end
    

    With basic ruby skills, you can write migrations. The second problem here is it is error prone because some queries might work well for different versions of MongoDB.

  2. gormigrate & goose: These solutions are an excellent choice because they allow us to define migrations with Go code. This is similar to the rails way shown above. See an example of gorm below:

    db, err := gorm.Open("sqlite3", "mydb.sqlite3")
    if err != nil {
    	log.Fatal(err)
    }
    
    db.LogMode(true)
    
    	m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
    		// add age column to persons
    		{
    			ID: "201608301415",
    			Migrate: func(tx *gorm.DB) error {
    				// when table already exists, it just adds fields as columns
    				type Person struct {
    					Age int
    				}
    				return tx.AutoMigrate(&Person{})
    			},
    			Rollback: func(tx *gorm.DB) error {
    				return tx.Migrator().DropColumn("people", "age")
    			},
    		}
    	})
    
    	if err = m.Migrate(); err != nil {
    		log.Fatalf("Could not migrate: %v", err)
    	}
    	log.Printf("Migration did run successfully")
    }
    

    The problem with these solutions again is - They don’t support MongoDB 😞

  3. Adhoc Scripts: This is the most common approach when you’re building closed-source projects. These projects don’t require that multiple users with multiple versions can be running in production simultaneously, and each user needs to be able to upgrade whenever they want. Adhoc Scripts don’t have a long life to live. They don’t have a linear history. Once applied in production they’re disposed. This won’t work for us!

Our Approach

Our approach was inspired by gormigrate, we refactored the same code to depend on mongoDB as well as not require schema migrations for MongoDB. With this, we end up with code like:

m := migrate.NewMigrator(c, opts, []*Migration{
			{
				ID: "201608301400",
				Migrate: func(db *mongo.Database) error {
					return nil
				},
				Rollback: func(db *mongo.Database) error {
					return nil
				},
			},
			{
				ID: "201608301430",
				Migrate: func(db *mongo.Database) error {
					return nil
				},
				Rollback: func(db *mongo.Database) error {
					return nil
				},
			},
		})

m.Migrate(context.Background())

You can find the full port over here.

Possible Future Work?

My next goal would be to upstream this port to either goose or gormigrate so this is useful to someone else out of the box.

Conclusion

I hope this helps someone thinking of using Golang and MongoDB in their project. Did I make an error in this article? Please let me know @subomiOluwalana

Bye for now 👋🏽

← Previous articleConvoy v0.6Next article →Receiving Webhooks from GitHub with Convoy

Related Posts

Nohooks: Webhooks for Platforms without Webhooks

June 28, 2023

Nohooks is a service that offers webhooks functionality even on platforms that do not have native webhooks support. With Nohooks, developers can receive real-time notifications of events from the list of supported apps like DigitalOcean, Render and Notion.

Subomi Oluwalana
Subomi Oluwalana

Co-Founder & CEO

Forward Proxies and Kubernetes Deployment

June 27, 2023

Configuring Convoy with Forward proxies and Deploying Convoy with our updated Kubernetes deployment helm charts

Dotun Jolaoso
Dotun Jolaoso

Backend Engineer

We switched Convoy's ID format from UUIDv4 to ULID

May 23, 2023

UUIDv4 and ULID are both types of unique identifiers that can be used in distributed systems. UUIDv4 stands for Universally Unique Identifier version 4, and ULID stands for Universally Unique Lexicographically Sortable Identifier.

Raymond Tukpe
Raymond Tukpe

CTO

Webhooks with Message Brokers and Convoy

February 17, 2023

Today, we’re excited to announce the upcoming release of our Message Broker integrations to ingest webhooks from backend services to Convoy. Starting in Convoy v0.9, API providers will be able to ingest webhook events from several backend services into Convoy using Message brokers, starting with Google PubSub and Amazon SQS.

Dotun Jolaoso
Dotun Jolaoso

Backend Engineer

logo

2261 Market Street, San Francisco, CA 94114

Companyaccordion icon

About Us

Trust Center

Terms of Use

Privacy Policy

DPA

Productaccordion icon

Open Source

Core Gateway

Self-Hosted Premium

Private Network

Free Trial

Convoy Playground

Use Casesaccordion icon

Fintech

AI & Machine Learning

Developer Tools

Healthcare

SaaS Platforms

E-commerce

Logistics

IoT & Devices

Resourcesaccordion icon

API Reference

Documentation

Blog

Status Page

Roadmap

What are Webhooks?

Convoy vs. Internal Implementation

Speak to usaccordion icon

Slack

[email protected]

Connect with us

Copyright 2026, All Rights Reserved

soc stamp