A Microservice Based Book Store Application with Golang,Postgresql and Docker
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:
- Create a .env file in both
book-service
andorder-service
directories. - Set environment variables in the .env files according to your PostgreSQL credentials.
- Create database tables
books
andorders
with appropriate columns. - 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 theorder-service
.env file to match the correct address ofbook-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.