├── .gitignore ├── .local.env ├── Dockerfile ├── Makefile ├── README.md ├── cmd ├── app │ └── main.go ├── relay │ └── main.go └── worker │ └── main.go ├── customer └── customer.go ├── database └── db.go ├── docker-compose.infras.yaml ├── docker-compose.yaml ├── docs ├── example.png └── without-outbox.png ├── go.mod ├── go.sum ├── img.png ├── queue └── queue.go └── shared └── outbox.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | 4 | .env -------------------------------------------------------------------------------- /.local.env: -------------------------------------------------------------------------------- 1 | DB_USER=root 2 | DB_PASS=123456 3 | DB_HOST=db 4 | DB_PORT=3306 5 | DB_NAME=outbox-demo 6 | 7 | RABBITMQ_USER=outbox 8 | RABBITMQ_PASS=123456 9 | RABBITMQ_HOST=rabbitmq 10 | RABBITMQ_PORT=5672 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine as builder 2 | RUN apk add --no-cache dpkg gcc git musl-dev openssh 3 | 4 | WORKDIR /app 5 | COPY go.mod . 6 | COPY go.sum . 7 | RUN go mod download 8 | COPY . . 9 | 10 | RUN CGO_ENABLED=0 GOOS=linux go build -a -v -installsuffix cgo -o app ./cmd/app 11 | RUN CGO_ENABLED=0 GOOS=linux go build -a -v -installsuffix cgo -o relay ./cmd/relay 12 | RUN CGO_ENABLED=0 GOOS=linux go build -a -v -installsuffix cgo -o worker ./cmd/worker 13 | 14 | 15 | FROM alpine:latest 16 | 17 | COPY --from=builder /app/app ./ 18 | COPY --from=builder /app/relay ./ 19 | COPY --from=builder /app/worker ./ 20 | 21 | CMD ["./app"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | infras: 2 | docker compose -f ./docker-compose.infras.yaml up -d 3 | 4 | dev: 5 | docker compose up 6 | 7 | build: 8 | docker compose build app 9 | 10 | clean: 11 | docker compose down && docker compose -f ./docker-compose.infras.yaml down -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Outbox pattern example 2 | 3 | ## Introduction 4 | 5 | A service command typically needs to update the database and publish messages/events. 6 | 7 | #### Examples: 8 | 9 | – sending an e-mail message after placing an order 10 | 11 | – sending an event about new client registration to the messaging system (RabbitMQ, Kafka, ...) 12 | 13 | #### Problem: 14 | 15 | How to reliably/atomically update the database and publish messages/events? 16 | 17 | ![without-outbox](docs/without-outbox.png) 18 | 19 | #### Solution 20 | 21 | We should implement the **Outbox Pattern**. A service that uses a relational database inserts messages/events into an outbox table in the same transaction. 22 | 23 | ## The Outbox Pattern 24 | 25 | ![outbox-pattern](https://microservices.io/i/patterns/data/ReliablePublication.png) 26 | (image from https://microservices.io) 27 | 28 | Outbox Pattern give us [At-Least-Once delivery](https://www.cloudcomputingpatterns.org/at_least_once_delivery/). The receiver application may receive the message more than once. 29 | 30 | #### How can a message receiver deal with duplicate messages? 31 | 32 | Design a receiver to be an [Idempotent Receiver](https://www.enterpriseintegrationpatterns.com/patterns/messaging/IdempotentReceiver.html) 33 | 34 | > The term idempotent is used in mathematics to describe a function that produces the same result if it is applied to itself, i.e. f(x) = f(f(x)). In Messaging this concepts translates into a message that has the same effect whether it is received once or multiple times. This means that a message can safely be resent without causing any problems even if the receiver receives duplicates of the same message. 35 | 36 | ## How to run example 37 | 38 | Example written in Golang, use Mysql as db and RabbitMQ as messaging system 39 | 40 | **Note:** This is just a simple solution, the system has thousands of messages per sec should consider a tool like [Debezium](https://debezium.io/) 41 | 42 | #### Example Design 43 | 44 | ![example-outbox](docs/example.png) 45 | 46 | Components 47 | 1. App: Backend API 48 | 2. Relay: Read message from outbox table and publish to RabbitMQ 49 | 3. Worker: Doing backgound jobs 50 | 51 | #### Build the system 52 | 53 | ```shell 54 | make build 55 | ``` 56 | 57 | #### Run example 58 | 59 | 1. Run the infras 60 | ```shell 61 | make infras 62 | ``` 63 | 64 | 2. Run the app, worker and relay. Open new terminal tab: 65 | 66 | ```shell 67 | make dev 68 | ``` 69 | 70 | 3. Cleaning 71 | 72 | ```shell 73 | make clean 74 | ``` 75 | 76 | 4. Send request. Open new terminal tab: 77 | 78 | ```shell 79 | curl -X POST -H "Content-Type: application/json" -d '{"email":"test@example.com","name":"TESTTTTTT"}' http://localhost:3000/customers 80 | ``` 81 | 82 | You will see logs like: 83 | 84 | ```shell 85 | outbox-demo-relay | 2021/08/04 09:37:16 Published messages: [fcb86b89-9404-48c7-9198-b85585ff8ab2] 86 | ``` 87 | 88 | ```shell 89 | outbox-demo-worker | 2021/08/04 09:37:16 Handling [CustomerCreated] - Payload: '{"id":"13db077f-b8b9-4512-9938-04f9aa00cae7","name":"TESTTTTTT","email":"test@example.com","CreatedAt":"2021-08-04T09:37:07.3053179Z","UpdatedAt":"0001-01-01T00:00:00Z"}' 90 | ``` -------------------------------------------------------------------------------- /cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/gofiber/fiber/v2/middleware/logger" 6 | "github.com/joho/godotenv" 7 | "log" 8 | "outbox/customer" 9 | "outbox/database" 10 | "outbox/shared" 11 | ) 12 | 13 | func main() { 14 | if err := godotenv.Load(); err != nil { 15 | log.Println("loading env file: ", err) 16 | } 17 | 18 | db, err := database.NewConnection() 19 | if err != nil { 20 | log.Fatal("error connecting to db") 21 | } 22 | 23 | if err := db.AutoMigrate(&customer.Customer{}, &shared.OutBoxMessage{}); err != nil { 24 | log.Fatal("migrate error - ", err) 25 | } 26 | 27 | customerHandler := customer.Handler{DB: db} 28 | 29 | app := fiber.New() 30 | 31 | app.Use(logger.New()) 32 | 33 | app.Post("/customers", customerHandler.Add) 34 | 35 | if err := app.Listen(":3000"); err != nil { 36 | log.Fatal(err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cmd/relay/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/joho/godotenv" 5 | "github.com/robfig/cron/v3" 6 | "io" 7 | "log" 8 | "os" 9 | "os/signal" 10 | "outbox/database" 11 | "outbox/queue" 12 | "outbox/shared" 13 | "syscall" 14 | ) 15 | 16 | func main() { 17 | if err := godotenv.Load(); err != nil { 18 | log.Println("loading env file: ", err) 19 | } 20 | 21 | db, err := database.NewConnection() 22 | if err != nil { 23 | log.Fatal("error connecting to db") 24 | } 25 | 26 | conn, err := queue.CreateConnection() 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | defer closeConnection(conn) 31 | 32 | ch, err := queue.CreateChannel(conn) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | defer closeConnection(ch) 37 | 38 | q, err := ch.QueueDeclare( 39 | "outbox", 40 | false, false, false, false, nil) 41 | 42 | jobProcessor := shared.OutboxProcesser{ 43 | DB: db, 44 | Channel: ch, 45 | Queue: q, 46 | } 47 | 48 | c := cron.New() 49 | _, err = c.AddFunc("@every 10s", jobProcessor.HandleOutboxMessage) 50 | if err != nil { 51 | log.Fatal("register handler error", err) 52 | } 53 | log.Println("Start processing outbox messages") 54 | c.Start() 55 | defer c.Stop() 56 | 57 | // Wait for terminate signal 58 | kill := make(chan os.Signal, 1) 59 | signal.Notify(kill, syscall.SIGINT, syscall.SIGTERM) 60 | <-kill 61 | } 62 | 63 | func closeConnection(c io.Closer) { 64 | err := c.Close() 65 | if err != nil { 66 | log.Println(err) 67 | } 68 | } -------------------------------------------------------------------------------- /cmd/worker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/joho/godotenv" 6 | "gorm.io/datatypes" 7 | "io" 8 | "log" 9 | "os" 10 | "os/signal" 11 | "outbox/queue" 12 | "syscall" 13 | ) 14 | 15 | type OutboxEvent struct { 16 | ID string `json:"id"` 17 | EventName string `json:"event_name"` 18 | Payload datatypes.JSON `json:"payload"` 19 | } 20 | 21 | func main() { 22 | if err := godotenv.Load(); err != nil { 23 | log.Println("loading env file: ", err) 24 | } 25 | 26 | conn, err := queue.CreateConnection() 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | defer closeConnection(conn) 31 | 32 | ch, err := queue.CreateChannel(conn) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | defer closeConnection(ch) 37 | 38 | q, err := ch.QueueDeclare( 39 | "outbox", 40 | false, false, false, false, nil) 41 | 42 | messages, err := ch.Consume( 43 | q.Name, 44 | "", // consumer 45 | true, // auto-ack 46 | false, // exclusive 47 | false, // no-local 48 | false, // no-wait 49 | nil, // args 50 | ) 51 | 52 | // Run in background 53 | go func() { 54 | log.Printf("Comsuming queue [%s] \n", q.Name) 55 | for m := range messages { 56 | var evt OutboxEvent 57 | if err := json.Unmarshal(m.Body, &evt); err != nil { 58 | log.Println("Handle message error: ", string(m.Body)) 59 | log.Println("ERR:", err) 60 | continue 61 | } 62 | log.Printf("Handling [%s] - Payload: '%s'", evt.EventName, evt.Payload) 63 | } 64 | }() 65 | 66 | // Wait for terminate signal 67 | kill := make(chan os.Signal, 1) 68 | signal.Notify(kill, syscall.SIGINT, syscall.SIGTERM) 69 | <-kill 70 | } 71 | 72 | func closeConnection(c io.Closer) { 73 | err := c.Close() 74 | if err != nil { 75 | log.Println(err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /customer/customer.go: -------------------------------------------------------------------------------- 1 | package customer 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gofiber/fiber/v2" 6 | "github.com/google/uuid" 7 | "gorm.io/datatypes" 8 | "gorm.io/gorm" 9 | "outbox/shared" 10 | "time" 11 | ) 12 | 13 | type Customer struct { 14 | ID string `json:"id" gorm:"id,primarykey"` 15 | Email string `json:"email"` 16 | Name string `json:"name"` 17 | CreatedAt time.Time 18 | UpdatedAt time.Time 19 | } 20 | 21 | type Handler struct { 22 | DB *gorm.DB 23 | } 24 | 25 | func (h *Handler) Add(c *fiber.Ctx) error { 26 | 27 | var customer Customer 28 | if err := c.BodyParser(&customer); err != nil { 29 | return err 30 | } 31 | customer.ID = uuid.NewString() 32 | customer.CreatedAt = time.Now() 33 | 34 | err := h.DB.Transaction(func(tx *gorm.DB) error { 35 | b, err := json.Marshal(customer) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | customerCreatedEvent := shared.OutBoxMessage{ 41 | ID: uuid.NewString(), 42 | EventName: "CustomerCreated", 43 | Payload: datatypes.JSON(b), 44 | IsProcessed: false, 45 | } 46 | 47 | if err := tx.FirstOrCreate(&customer).Error; err != nil { 48 | return err 49 | } 50 | 51 | if err := tx.Create(&customerCreatedEvent).Error; err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | }) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "gorm.io/driver/mysql" 6 | "gorm.io/gorm" 7 | "os" 8 | ) 9 | 10 | func NewConnection() (*gorm.DB, error) { 11 | dsn := fmt.Sprintf( 12 | "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", 13 | os.Getenv("DB_USER"), 14 | os.Getenv("DB_PASS"), 15 | os.Getenv("DB_HOST"), 16 | os.Getenv("DB_PORT"), 17 | os.Getenv("DB_NAME"), 18 | ) 19 | return gorm.Open(mysql.Open(dsn), &gorm.Config{}) 20 | } 21 | -------------------------------------------------------------------------------- /docker-compose.infras.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | 5 | db: 6 | image: mysql:5.7 7 | environment: 8 | MYSQL_ROOT_PASSWORD: "123456" 9 | MYSQL_DATABASE: "outbox-demo" 10 | MYSQL_USER: "outbox" 11 | MYSQL_PASSWORD: "123456" 12 | ports: 13 | - "33063:3306" 14 | 15 | rabbitmq: 16 | image: rabbitmq:3.8-management-alpine 17 | environment: 18 | RABBITMQ_DEFAULT_PASS: "123456" 19 | RABBITMQ_DEFAULT_USER: "outbox" 20 | ports: 21 | - "56720:5672" 22 | - "8081:15672" 23 | 24 | networks: 25 | default: -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | 5 | app: 6 | image: outbox-demo 7 | container_name: outbox-demo-app 8 | restart: on-failure 9 | build: 10 | context: . 11 | env_file: 12 | - .local.env 13 | ports: 14 | - "3000:3000" 15 | command: ["./app"] 16 | 17 | relay: 18 | image: outbox-demo 19 | container_name: outbox-demo-relay 20 | restart: on-failure 21 | build: 22 | context: . 23 | env_file: 24 | - .local.env 25 | command: [ "./relay" ] 26 | 27 | worker: 28 | image: outbox-demo 29 | container_name: outbox-demo-worker 30 | restart: on-failure 31 | build: 32 | context: . 33 | env_file: 34 | - .local.env 35 | command: [ "./worker" ] 36 | 37 | networks: 38 | outbox: -------------------------------------------------------------------------------- /docs/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngoctrng/golang-outbox-example/de8d2bd9e854db899edcd5d80f467620f4e3cea7/docs/example.png -------------------------------------------------------------------------------- /docs/without-outbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngoctrng/golang-outbox-example/de8d2bd9e854db899edcd5d80f467620f4e3cea7/docs/without-outbox.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module outbox 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.16.0 7 | github.com/google/uuid v1.3.0 8 | github.com/joho/godotenv v1.3.0 9 | github.com/robfig/cron/v3 v3.0.0 10 | github.com/streadway/amqp v1.0.0 11 | gorm.io/datatypes v1.0.1 12 | gorm.io/driver/mysql v1.1.1 13 | gorm.io/gorm v1.21.12 14 | ) 15 | 16 | require ( 17 | github.com/andybalholm/brotli v1.0.2 // indirect 18 | github.com/go-sql-driver/mysql v1.6.0 // indirect 19 | github.com/jinzhu/inflection v1.0.0 // indirect 20 | github.com/jinzhu/now v1.1.2 // indirect 21 | github.com/klauspost/compress v1.12.2 // indirect 22 | github.com/valyala/bytebufferpool v1.0.0 // indirect 23 | github.com/valyala/fasthttp v1.26.0 // indirect 24 | github.com/valyala/tcplisten v1.0.0 // indirect 25 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= 3 | github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 4 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 5 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 6 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 7 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk= 11 | github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 12 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 13 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 14 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 15 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 16 | github.com/gofiber/fiber/v2 v2.16.0 h1:Bly40vAh4qofpCoVYGLYC0TS9lNGNA1OVSPuzhIK7Q8= 17 | github.com/gofiber/fiber/v2 v2.16.0/go.mod h1:iftruuHGkRYGEXVISmdD7HTYWyfS2Bh+Dkfq4n/1Owg= 18 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 19 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 20 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 21 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 22 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 23 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 24 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 25 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 26 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 27 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 28 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 29 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 30 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 31 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 32 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 33 | github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= 34 | github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= 35 | github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= 36 | github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw= 37 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 38 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 39 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 40 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 41 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 42 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 43 | github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 44 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 45 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 46 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 47 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 48 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 49 | github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 50 | github.com/jackc/pgproto3/v2 v2.0.6 h1:b1105ZGEMFe7aCvrT1Cca3VoVb4ZFMaFJLJcg/3zD+8= 51 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 52 | github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 53 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 54 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 55 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 56 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 57 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 58 | github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= 59 | github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= 60 | github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= 61 | github.com/jackc/pgtype v1.6.2 h1:b3pDeuhbbzBYcg5kwNmNDun4pFUD/0AAr1kLXZLeNt8= 62 | github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= 63 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 64 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 65 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 66 | github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= 67 | github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= 68 | github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= 69 | github.com/jackc/pgx/v4 v4.10.1 h1:/6Q3ye4myIj6AaplUm+eRcz4OhK9HAvFf4ePsG40LJY= 70 | github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= 71 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 72 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 73 | github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 74 | github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 75 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 76 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 77 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 78 | github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 79 | github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= 80 | github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 81 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 82 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 83 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 84 | github.com/klauspost/compress v1.12.2 h1:2KCfW3I9M7nSc5wOqXAlW2v2U6v+w6cbjvbfp+OykW8= 85 | github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 86 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 87 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 88 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 89 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 90 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 91 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 92 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 93 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 94 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 95 | github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 96 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 97 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 98 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 99 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 100 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 101 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 102 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 103 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 104 | github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= 105 | github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= 106 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 107 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 108 | github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= 109 | github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 110 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 111 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 112 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 113 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 114 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 115 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 116 | github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 117 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 118 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 119 | github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= 120 | github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 121 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 122 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 123 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 124 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 125 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 126 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 127 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 128 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 129 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 130 | github.com/valyala/fasthttp v1.26.0 h1:k5Tooi31zPG/g8yS6o2RffRO2C9B9Kah9SY8j/S7058= 131 | github.com/valyala/fasthttp v1.26.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA= 132 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 133 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 134 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 135 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 136 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 137 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 138 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 139 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 140 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 141 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 142 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 143 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 144 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 145 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 146 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 147 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 148 | golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 149 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 150 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 151 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 152 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= 153 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 154 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 155 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 156 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 157 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 158 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 159 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 160 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 161 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 162 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 163 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 164 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 165 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 166 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 170 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 172 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 174 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= 176 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 177 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 178 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 179 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 180 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 181 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 182 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 183 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 184 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 185 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 186 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 187 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 188 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 189 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 190 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 191 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 192 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 193 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 194 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 195 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 196 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 197 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 198 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 199 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 200 | gorm.io/datatypes v1.0.1 h1:6npnXbBtjpSb7FFVA2dG/llyTN8tvZfbUqs+WyLrYgQ= 201 | gorm.io/datatypes v1.0.1/go.mod h1:HEHoUU3/PO5ZXfAJcVWl11+zWlE16+O0X2DgJEb4Ixs= 202 | gorm.io/driver/mysql v1.0.5/go.mod h1:N1OIhHAIhx5SunkMGqWbGFVeh4yTNWKmMo1GOAsohLI= 203 | gorm.io/driver/mysql v1.1.1 h1:yr1bpyqiwuSPJ4aGGUX9nu46RHXlF8RASQVb1QQNcvo= 204 | gorm.io/driver/mysql v1.1.1/go.mod h1:KdrTanmfLPPyAOeYGyG+UpDys7/7eeWT1zCq+oekYnU= 205 | gorm.io/driver/postgres v1.0.8 h1:PAgM+PaHOSAeroTjHkCHCBIHHoBIf9RgPWGo8dF2DA8= 206 | gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= 207 | gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= 208 | gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= 209 | gorm.io/driver/sqlserver v1.0.7 h1:uwUtb0kdFwW5PkRbd2KJ2h4wlsqvLSjox1XVg/RnzRE= 210 | gorm.io/driver/sqlserver v1.0.7/go.mod h1:ng66aHI47ZIKz/vvnxzDoonzmTS8HXP+JYlgg67wOog= 211 | gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 212 | gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 213 | gorm.io/gorm v1.21.3/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 214 | gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 215 | gorm.io/gorm v1.21.6/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 216 | gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 217 | gorm.io/gorm v1.21.12 h1:3fQM0Eiz7jcJEhPggHEpoYnsGZqynMzverL77DV40RM= 218 | gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 219 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 220 | -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngoctrng/golang-outbox-example/de8d2bd9e854db899edcd5d80f467620f4e3cea7/img.png -------------------------------------------------------------------------------- /queue/queue.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "fmt" 5 | "github.com/streadway/amqp" 6 | "os" 7 | ) 8 | 9 | func CreateConnection() (*amqp.Connection, error) { 10 | ampqURL := fmt.Sprintf( 11 | "amqp://%s:%s@%s:%s", 12 | os.Getenv("RABBITMQ_USER"), os.Getenv("RABBITMQ_PASS"), 13 | os.Getenv("RABBITMQ_HOST"), os.Getenv("RABBITMQ_PORT")) 14 | return amqp.Dial(ampqURL) 15 | } 16 | 17 | func CreateChannel(conn *amqp.Connection) (*amqp.Channel, error) { 18 | return conn.Channel() 19 | } 20 | -------------------------------------------------------------------------------- /shared/outbox.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/streadway/amqp" 6 | "gorm.io/datatypes" 7 | "gorm.io/gorm" 8 | "log" 9 | "time" 10 | ) 11 | 12 | type OutBoxMessage struct { 13 | ID string `gorm:"id" json:"id"` 14 | EventName string `gorm:"event_name" json:"event_name"` 15 | Payload datatypes.JSON `gorm:"payload" json:"payload"` 16 | IsProcessed bool `gorm:"is_processed" json:"is_processed"` 17 | } 18 | 19 | type OutboxProcesser struct { 20 | DB *gorm.DB 21 | Channel *amqp.Channel 22 | Queue amqp.Queue 23 | } 24 | 25 | func (p *OutboxProcesser) HandleOutboxMessage() { 26 | messages := make([]OutBoxMessage, 0) 27 | err := p.DB. 28 | Where("is_processed = ?", false). 29 | Find(&messages).Error 30 | if err != nil { 31 | log.Println("query outbox messages error: ", err) 32 | return 33 | } 34 | 35 | // no waiting message 36 | if len(messages) == 0 { 37 | return 38 | } 39 | 40 | // Publish each message. 41 | // If success, add to processed slice 42 | processedID := make([]string, 0) 43 | for _, m := range messages { 44 | b, err := json.Marshal(m) 45 | if err != nil { 46 | continue 47 | } 48 | 49 | // publish message to a queue 50 | if err := p.publishMessage(b); err != nil { 51 | log.Println("publish outbox message error: ", err) 52 | continue 53 | } 54 | 55 | processedID = append(processedID, m.ID) 56 | } 57 | 58 | // Update processed messages in database 59 | err = p.DB.Model(&OutBoxMessage{}). 60 | Where("id IN ?", processedID). 61 | UpdateColumn("is_processed", true).Error 62 | if err != nil { 63 | log.Println("update outbox error: ", err) 64 | return 65 | } 66 | 67 | log.Println("Published messages:", processedID) 68 | } 69 | 70 | func (p *OutboxProcesser) publishMessage(body []byte) error { 71 | return p.Channel.Publish( 72 | "", 73 | p.Queue.Name, 74 | false, 75 | false, 76 | amqp.Publishing{ 77 | ContentType: "application/json", 78 | Timestamp: time.Now(), 79 | Body: body, 80 | }, 81 | ) 82 | } 83 | --------------------------------------------------------------------------------