A Microservice Based Book Store Application with Golang,Postgresql and Docker

Ferdous Azad
7 min readJun 15, 2024

--

Microservices for a Book Store Application

This example demonstrates two microservices: book-service and order-service for a simple book store application.

1. book-service:

  • Manages book data (title, author, ISBN, price).
  • Exposes endpoints for listing books, fetching details, and adding new books.

2. order-service:

  • Handles customer orders.
  • Exposes endpoints for placing orders, retrieving order history, and updating order status.
bookstore-app/
├── book-service/
│ ├── main.go
│ ├── config.go
│ ├── handlers.go
│ ├── models.go
│ └── db.go
├── order-service/
│ ├── main.go
│ ├── config.go
│ ├── handlers.go
│ ├── models.go
│ └── db.go
└── docker-compose.yml

book-service/main.go

package main

import (
"fmt"
"log"
"net/http"

"github.com/gorilla/mux"
"github.com/joho/godotenv"

"bookstore-app/book-service/config"
"bookstore-app/book-service/db"
"bookstore-app/book-service/handlers"
)

func main() {
// Load environment variables
err := godotenv.Load(".env")
if err != nil {
log.Fatal("Error loading environment variables:", err)
}

// Connect to PostgreSQL database
db, err := db.ConnectDB(config.GetConfig())
if err != nil {
log.Fatal("Error connecting to database:", err)
}
defer db.Close()

// Create a new router
router := mux.NewRouter()

// Define book service endpoints
router.HandleFunc("/books", handlers.GetBooks(db)).Methods("GET")
router.HandleFunc("/books/{id}", handlers.GetBookByID(db)).Methods("GET")
router.HandleFunc("/books", handlers.AddBook(db)).Methods("POST")

// Start the server
fmt.Printf("Book service listening on port %s\n", config.GetConfig().Port)
log.Fatal(http.ListenAndServe(config.GetConfig().Port, router))
}

book-service/config.go

package config

import (
"fmt"
"os"
)

type Config struct {
Port string
DbHost string
DbPort string
DbUser string
DbPass string
DbName string
DbSslMode string
}

func GetConfig() Config {
return Config{
Port: os.Getenv("PORT"),
DbHost: os.Getenv("DB_HOST"),
DbPort: os.Getenv("DB_PORT"),
DbUser: os.Getenv("DB_USER"),
DbPass: os.Getenv("DB_PASS"),
DbName: os.Getenv("DB_NAME"),
DbSslMode: os.Getenv("DB_SSL_MODE"),
}
}

func (c *Config) GetDbConnectionString() string {
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
c.DbHost, c.DbPort, c.DbUser, c.DbPass, c.DbName, c.DbSslMode)
}

book-service/handlers.go

package handlers

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"

"bookstore-app/book-service/db"
"bookstore-app/book-service/models"
)

func GetBooks(db *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
books, err := db.GetBooks()
if err != nil {
http.Error(w, fmt.Sprintf("Error fetching books: %v", err), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(books)
}
}

func GetBookByID(db *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
book, err := db.GetBookByID(id)
if err != nil {
http.Error(w, fmt.Sprintf("Error fetching book: %v", err), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(book)
}
}

func AddBook(db *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("Error reading request body: %v", err), http.StatusBadRequest)
return
}
var book models.Book
err = json.Unmarshal(body, &book)
if err != nil {
http.Error(w, fmt.Sprintf("Error decoding request body: %v", err), http.StatusBadRequest)
return
}
err = db.AddBook(&book)
if err != nil {
http.Error(w, fmt.Sprintf("Error adding book: %v", err), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(book)
}
}

book-service/models.go

package models

type Book struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
ISBN string `json:"isbn"`
Price int `json:"price"`
}

book-service/db.go

package db

import (
"database/sql"
"fmt"

_ "github.com/lib/pq"

"bookstore-app/book-service/config"
"bookstore-app/book-service/models"
)

type DB struct {
*sql.DB
}

func ConnectDB(cfg *config.Config) (*DB, error) {
db, err := sql.Open("postgres", cfg.GetDbConnectionString())
if err != nil {
return nil, fmt.Errorf("error connecting to database: %v", err)
}

// Check the database connection
err = db.Ping()
if err != nil {
return nil, fmt.Errorf("error pinging database: %v", err)
}

return &DB{db}, nil
}

func (db *DB) GetBooks() ([]models.Book, error) {
rows, err := db.Query("SELECT * FROM books")
if err != nil {
return nil, fmt.Errorf("error getting books: %v", err)
}
defer rows.Close()

var books []models.Book
for rows.Next() {
var book models.Book
err = rows.Scan(&book.ID, &book.Title, &book.Author, &book.ISBN, &book.Price)
if err != nil {
return nil, fmt.Errorf("error scanning row: %v", err)
}
books = append(books, book)
}

return books, nil
}

func (db *DB) GetBookByID(id string) (*models.Book, error) {
var book models.Book
err := db.QueryRow("SELECT * FROM books WHERE id = $1", id).Scan(&book.ID, &book.Title, &book.Author, &book.ISBN, &book.Price)
if err != nil {
return nil, fmt.Errorf("error getting book by ID: %v", err)
}

return &book, nil
}

func (db *DB) AddBook(book *models.Book) error {
_, err := db.Exec("INSERT INTO books (title, author, isbn, price) VALUES ($1, $2, $3, $4)",
book.Title, book.Author, book.ISBN, book.Price)
if err != nil {
return fmt.Errorf("error adding book: %v", err)
}
return nil
}

order-service/main.go

package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"

"github.com/gorilla/mux"
"github.com/joho/godotenv"

"bookstore-app/order-service/config"
"bookstore-app/order-service/db"
"bookstore-app/order-service/handlers"
"bookstore-app/order-service/models"
)

func main() {
// Load environment variables
err := godotenv.Load(".env")
if err != nil {
log.Fatal("Error loading environment variables:", err)
}

// Connect to PostgreSQL database
db, err := db.ConnectDB(config.GetConfig())
if err != nil {
log.Fatal("Error connecting to database:", err)
}
defer db.Close()

// Create a new router
router := mux.NewRouter()

// Define order service endpoints
router.HandleFunc("/orders", handlers.PlaceOrder(db)).Methods("POST")
router.HandleFunc("/orders/{id}", handlers.GetOrderByID(db)).Methods("GET")
router.HandleFunc("/orders/{id}", handlers.UpdateOrderStatus(db)).Methods("PUT")

// Start the server
fmt.Printf("Order service listening on port %s\n", config.GetConfig().Port)
log.Fatal(http.ListenAndServe(config.GetConfig().Port, router))
}

order-service/config.go

package config

import (
"fmt"
"os"
)

type Config struct {
Port string
DbHost string
DbPort string
DbUser string
DbPass string
DbName string
DbSslMode string
BookSvcURL string
}

func GetConfig() Config {
return Config{
Port: os.Getenv("PORT"),
DbHost: os.Getenv("DB_HOST"),
DbPort: os.Getenv("DB_PORT"),
DbUser: os.Getenv("DB_USER"),
DbPass: os.Getenv("DB_PASS"),
DbName: os.Getenv("DB_NAME"),
DbSslMode: os.Getenv("DB_SSL_MODE"),
BookSvcURL: os.Getenv("BOOK_SVC_URL"),
}
}

func (c *Config) GetDbConnectionString() string {
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
c.DbHost, c.DbPort, c.DbUser, c.DbPass, c.DbName, c.DbSslMode)
}

order-service/handlers.go

package handlers

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"

"bookstore-app/order-service/config"
"bookstore-app/order-service/db"
"bookstore-app/order-service/models"
)

func PlaceOrder(db *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("Error reading request body: %v", err), http.StatusBadRequest)
return
}
var order models.Order
err = json.Unmarshal(body, &order)
if err != nil {
http.Error(w, fmt.Sprintf("Error decoding request body: %v", err), http.StatusBadRequest)
return
}
err = db.PlaceOrder(&order)
if err != nil {
http.Error(w, fmt.Sprintf("Error placing order: %v", err), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(order)
}
}

func GetOrderByID(db *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
order, err := db.GetOrderByID(id)
if err != nil {
http.Error(w, fmt.Sprintf("Error fetching order: %v", err), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(order)
}
}

func UpdateOrderStatus(db *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("Error reading request body: %v", err), http.StatusBadRequest)
return
}
var status models.OrderStatus
err = json.Unmarshal(body, &status)
if err != nil {
http.Error(w, fmt.Sprintf("Error decoding request body: %v", err), http.StatusBadRequest)
return
}
err = db.UpdateOrderStatus(id, status)
if err != nil {
http.Error(w, fmt.Sprintf("Error updating order status: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}

func GetBookDetails(bookID string) (*models.Book, error) {
cfg := config.GetConfig()
url := fmt.Sprintf("%s/books/%s", cfg.BookSvcURL, bookID)

resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("error fetching book details: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error fetching book details: %s", resp.Status)
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %v", err)
}

var book models.Book
err = json.Unmarshal(body, &book)
if err != nil {
return nil, fmt.Errorf("error decoding response body: %v", err)
}

return &book, nil
}

order-service/models.go

package models

import "time"

type Order struct {
ID string `json:"id"`
CustomerID string `json:"customer_id"`
BookID string `json:"book_id"`
Quantity int `json:"quantity"`
Status OrderStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

type OrderStatus string

const (
Pending OrderStatus = "pending"
Processing OrderStatus = "processing"
Shipped OrderStatus = "shipped"
Completed OrderStatus = "completed"
Cancelled OrderStatus = "cancelled"
)

type Book struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
ISBN string `json:"isbn"`
Price int `json:"price"`
}

order-service/db.go

package db

import (
"database/sql"
"fmt"
"time"

_ "github.com/lib/pq"

"bookstore-app/order-service/config"
"bookstore-app/order-service/handlers"
"bookstore-app/order-service/models"
)

type DB struct {
*sql.DB
}

func ConnectDB(cfg *config.Config) (*DB, error) {
db, err := sql.Open("postgres", cfg.GetDbConnectionString())
if err != nil {
return nil, fmt.Errorf("error connecting to database: %v", err)
}

// Check the database connection
err = db.Ping()
if err != nil {
return nil, fmt.Errorf("error pinging database: %v", err)
}

return &DB{db}, nil
}

func (db *DB) PlaceOrder(order *models.Order) error {
book, err := handlers.GetBookDetails(order.BookID)
if err != nil {
return fmt.Errorf("error fetching book details: %v", err)
}
order.CreatedAt = time.Now()
order.UpdatedAt = time.Now()
order.Status = models.Pending
_, err = db.Exec("INSERT INTO orders (customer_id, book_id, quantity, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)",
order.CustomerID, order.BookID, order.Quantity, order.Status, order.CreatedAt, order.UpdatedAt)
if err != nil {
return fmt.Errorf("error placing order: %v", err)
}
return nil
}

func (db *DB) GetOrderByID(id string) (*models.Order, error) {
var order models.Order
err := db.QueryRow("SELECT * FROM orders WHERE id = $1", id).Scan(&order.ID, &order.CustomerID, &order.BookID, &order.Quantity, &order.Status, &order.CreatedAt, &order.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("error getting order by ID: %v", err)
}
return &order, nil
}

func (db *DB) UpdateOrderStatus(id string, status models.OrderStatus) error {
_, err := db.Exec("UPDATE orders SET status = $1, updated_at = $2 WHERE id = $3", status, time.Now(), id)
if err != nil {
return fmt.Errorf("error updating order status: %v", err)
}
return nil
}

docker-compose.yml

version: "3.7"

services:
book-service:
build: ./book-service
ports:
- "8080:8080"
environment:
- PORT=8080
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=postgres
- DB_PASS=postgres
- DB_NAME=bookstore
- DB_SSL_MODE=disable
depends_on:
- postgres

order-service:
build: ./order-service
ports:
- "8081:8081"
environment:
- PORT=8081
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=postgres
- DB_PASS=postgres
- DB_NAME=bookstore
- DB_SSL_MODE=disable
- BOOK_SVC_URL=http://book-service:8080
depends_on:
- postgres

postgres:
image: postgres:latest
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=bookstore
ports:
- "5432:5432"

Instructions:

  1. Create a .env file in both book-service and order-service directories.
  2. Set environment variables in the .env files according to your PostgreSQL credentials.
  3. Create database tables books and orders with appropriate columns.
  4. Run docker-compose up -d to start the services.

Notes:

  • This example uses godotenv for environment variables.
  • The services are communicating using HTTP requests.
  • You need to adjust the BOOK_SVC_URL in the order-service .env file to match the correct address of book-service.
  • This is a basic example and can be expanded with features like authentication, error handling, and more complex communication patterns.
  • Remember to replace the placeholders in the Dockerfile with your actual build context.

This provides a basic framework for building microservices using Go, PostgreSQL, and Docker. You can further customize this structure by adding more services, implementing communication protocols, and introducing advanced features like observability and distributed tracing.

--

--

Ferdous Azad

Go Developer | Microservices Enthusiast | Open Source Contributor