Skip to content

Adapter is a Go project that demonstrates the Adapter Pattern (also known as Repository Pattern) for building loosely-coupled, maintainable applications. It shows how to use Go interfaces to create database-agnostic code that's easy to test, extend, and maintain

License

Notifications You must be signed in to change notification settings

kushalsubedi/Adapter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

How Interfaces Enable Application Decoupling in Go

Introduction

One of the most powerful features in Go is its interface system. Unlike many other languages, Go's interfaces are implicit, lightweight, and incredibly effective at decoupling your application components. In this article, we'll explore how interfaces can transform a tightly-coupled codebase into a flexible, maintainable architecture.

The Problem: Tight Coupling

Let's start with a common scenario. You're building a user management system with PostgreSQL:

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/lib/pq"
)

type User struct {
    ID   int
    Name string
}

type UserService struct {
    postgresDB *sql.DB  // Directly depends on PostgreSQL
}

func NewUserService(db *sql.DB) *UserService {
    return &UserService{postgresDB: db}
}

func (s *UserService) RegisterUser(user User) error {
    _, err := s.postgresDB.Exec(
        "INSERT INTO users (name) VALUES ($1)",  // PostgreSQL syntax
        user.Name,
    )
    return err
}

func (s *UserService) ListUsers() ([]User, error) {
    rows, err := s.postgresDB.Query("SELECT id, name FROM users")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        rows.Scan(&u.ID, &u.Name)
        users = append(users, u)
    }
    return users, nil
}

What's Wrong Here?

This code has several critical issues:

  1. Database Lock-in: Your business logic (UserService) is tightly coupled to PostgreSQL
  2. Testing Nightmare: You need a real database to test your business logic
  3. No Flexibility: Switching to MySQL, MongoDB, or even a mock implementation requires rewriting UserService
  4. Violation of SOLID Principles: Specifically, the Dependency Inversion Principle

The Solution: Interface-Based Decoupling

Let's refactor this using Go interfaces to achieve decoupling.

Step 1: Define the Interface

First, we define what operations we need, not how they're implemented:

// repository/repository.go
package repository

import "project/models"

// UserRepository defines the contract for user data access
type UserRepository interface {
    Create(user models.User) error
    GetAll() ([]models.User, error)
}

Key Insight: This interface represents a contract. Any type that implements these methods satisfies the interface, no explicit declaration needed.

Step 2: Implement for PostgreSQL

// repository/postgres.go
package repository

import (
    "database/sql"
    "fmt"
    "project/models"
)

type PostgresRepo struct {
    db *sql.DB
}

func NewPostgresRepo(db *sql.DB) *PostgresRepo {
    return &PostgresRepo{db: db}
}

func (p *PostgresRepo) Create(user models.User) error {
    res, err := p.db.Exec(
        "INSERT INTO users (name) VALUES ($1)",  // PostgreSQL-specific
        user.Name,
    )
    if err != nil {
        return fmt.Errorf("failed to insert user: %w", err)
    }
    
    rows, err := res.RowsAffected()
    if err != nil {
        return fmt.Errorf("failed to get rows affected: %w", err)
    }
    
    fmt.Println("Inserted rows:", rows)
    return nil
}

func (p *PostgresRepo) GetAll() ([]models.User, error) {
    rows, err := p.db.Query("SELECT id, name FROM users")
    if err != nil {
        return nil, fmt.Errorf("failed to query users: %w", err)
    }
    defer rows.Close()

    var users []models.User
    for rows.Next() {
        var u models.User
        if err := rows.Scan(&u.ID, &u.Name); err != nil {
            return nil, fmt.Errorf("failed to scan user: %w", err)
        }
        users = append(users, u)
    }

    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("error iterating rows: %w", err)
    }

    return users, nil
}

Step 3: Implement for MySQL

Here's where the magic happens , we can add MySQL support without touching existing code:

// repository/mysql.go
package repository

import (
    "database/sql"
    "fmt"
    "project/models"
)

type MySQLRepo struct {
    db *sql.DB
}

func NewMySQLRepo(db *sql.DB) *MySQLRepo {
    return &MySQLRepo{db: db}
}

func (m *MySQLRepo) Create(user models.User) error {
    _, err := m.db.Exec(
        "INSERT INTO users (name) VALUES (?)",  // MySQL syntax (different!)
        user.Name,
    )
    if err != nil {
        return fmt.Errorf("failed to insert user: %w", err)
    }
    return nil
}

func (m *MySQLRepo) GetAll() ([]models.User, error) {
    rows, err := m.db.Query("SELECT id, name FROM users")
    if err != nil {
        return nil, fmt.Errorf("failed to query users: %w", err)
    }
    defer rows.Close()

    var users []models.User
    for rows.Next() {
        var u models.User
        if err := rows.Scan(&u.ID, &u.Name); err != nil {
            return nil, fmt.Errorf("failed to scan user: %w", err)
        }
        users = append(users, u)
    }

    return users, nil
}

Notice: Both PostgresRepo and MySQLRepo satisfy the UserRepository interface without explicitly declaring it. This is Go's implicit interface satisfaction.

Step 4: Decouple the Service Layer

Now our service layer depends on the interface, not concrete implementations:

// service/user_service.go
package service

import (
    "fmt"
    "project/models"
    "project/repository"
)

type UserService struct {
    repo repository.UserRepository  // Depends on interface, not implementation!
}

func NewUserService(repo repository.UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) RegisterUser(name string) error {
    // Business logic validation
    if name == "" {
        return fmt.Errorf("user name cannot be empty")
    }

    user := models.User{Name: name}
    if err := s.repo.Create(user); err != nil {
        return fmt.Errorf("failed to register user: %w", err)
    }

    return nil
}

func (s *UserService) ListUsers() ([]models.User, error) {
    users, err := s.repo.GetAll()
    if err != nil {
        return nil, fmt.Errorf("failed to list users: %w", err)
    }
    return users, nil
}

Critical Point: UserService has zero knowledge about PostgreSQL, MySQL, or any specific database. It only knows about the UserRepository interface.

The Decoupling

1. Easy Database Switching

In your main function, switching databases is trivial:

// main.go
package main

import (
    "log"
    "project/config"
    "project/repository"
    "project/service"
)

func main() {
    // Option 1: Use PostgreSQL
    pgConfig := config.DatabaseConfig{
        Host:     "localhost",
        Port:     5433,
        User:     "postgres",
        Password: "postgres",
        DBName:   "appdb",
        SSLMode:  "disable",
    }
    
    db, err := config.NewPostgresConnection(pgConfig)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    
    repo := repository.NewPostgresRepo(db)
    
    // Option 2: Switch to MySQL - just change these two lines!
    // db, err := config.NewMySQLConnection(mysqlConfig)
    // repo := repository.NewMySQLRepo(db)
    
    // The rest of the code remains identical
    userService := service.NewUserService(repo)
    
    users, _ := userService.ListUsers()
    fmt.Println("Users:", users)
}

About

Adapter is a Go project that demonstrates the Adapter Pattern (also known as Repository Pattern) for building loosely-coupled, maintainable applications. It shows how to use Go interfaces to create database-agnostic code that's easy to test, extend, and maintain

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages