DEV Community

Cover image for The Strategy Pattern in Go: A Simple Guide to Writing Extensible Code
Archit Agarwal
Archit Agarwal

Posted on • Originally published at linkedin.com

1

The Strategy Pattern in Go: A Simple Guide to Writing Extensible Code

Imagine you're running a coffee shop ☕, and customers ask for their order receipts in different formats - some want their receipts printed, some in PDF sent over Email, and one guy asks for a receipt over SMS. Now, imagine that you have written this billing software to support a printed receipt.🫣
Every time a new type of receipt is requested, you rewrite your entire billing system. Sounds painful, right? Well, that's precisely what happened in our Go code when UserService was initially responsible for exporting users as JSON and CSV, and now we onboard a client who wants this in XML. Let's look at the initial UserService.

Initial UserService: Without using Strategy Design Pattern

Imagine you have a UserService that fetches user data and allows exporting it in different formats. Right now, you support JSON and CSV, but what if tomorrow you need XML, PDF, or Parquet?

var (
 Err_NotSupportedExportType = errors.New("Not Supported export Type")
)

type IUserPersistence interface {
 ListUsers(paginationObj models.Pagination) []models.UserModel
}
type UserService struct {
 userPersistence IUserPersistence
}

func InitUserService(userPersistence IUserPersistence) *UserService {
 return &UserService{
  userPersistence: userPersistence,
 }
}

func (userSvc *UserService) GetUsers(paginationObj models.Pagination) []models.UserModel {
 return userSvc.userPersistence.ListUsers(paginationObj)
}

func (userSvc *UserService) ExportUsers(paginationObj models.Pagination, 
exportConfig models.ExportConfig) (string, error) {

 users := userSvc.GetUsers(paginationObj)

switch exportConfig.Type {
 case enums.ExportType_Json:
  jsonData, err := json.Marshal(users)
  return string(jsonData), err
 case enums.ExportType_CSV:
  sb := strings.Builder{}
  sb.WriteString("id,name,email\n")
  for _, u := range users {
   sb.WriteString(fmt.Sprintf("%d,%s,%s\n", u.Id, u.Name, u.Email))
  }
  return sb.String(), nil
 default:
  return "", Err_NotSupportedExportType
 }
}
Enter fullscreen mode Exit fullscreen mode

The Problem with This Approach

With the current implementation, every time you add a new export format, you modify the ExportUsers method. This violates two key SOLID principles:

  • Single Responsibility Principle (SRP) - UserService should focus only on fetching users, not handling export logic.
  • Open/Closed Principle (OCP) - The class should be open for extension but closed for modification. Right now, adding a new export type means editing existing code, increasing the risk of breaking something.

Issues with this approach:

  • 💔 UserService becomes a bottleneck.
  • 💀 Every new format requires modifying existing code.
  • 💥 One small change could break everything.
  • 😭 Teams adding new export formats need to touch your code.
  • 😫 Testing becomes harder as the service grows.

How Can We Fix This?

Instead of making UserService aware of every export format, we need a pluggable solution. This is where the Strategy Pattern shines! ✨

💡 Introducing the Strategy Pattern

Think of it like choosing a shipping method on Amazon: You pick Standard Shipping, Express, or Same-Day - but Amazon doesn't care how it's delivered. The delivery company handles that.
Similarly, our UserService should just fetch users and let the export strategy decide how to format them.

Are you ready to start the code repair 👩🏻‍🔧?

Step 1: Define a Common Export Interface

// Strategy interface
type IUserExport interface {
 Export(data []models.UserModel) (string, error)
}
Enter fullscreen mode Exit fullscreen mode

Implement Concrete Strategies

// CSV Export: Concrete Strategy
type CsvExportStrategy struct {
}

func (e *CsvExportStrategy) Export(data []models.UserModel) (string, error) {
 if len(data) == 0 {
  return "", errors.New("No data")
 }
 sb := strings.Builder{}
 sb.WriteString(data[0].GetHeader())
 for _, u := range data {
  sb.WriteString(u.GetRow())
 }
 return sb.String(), nil
}

// JSON Export: Concrete Strategy
type JSONExportStrategy struct {
}

func (e *JSONExportStrategy) Export(data []models.UserModel) (string, error) {
 if len(data) == 0 {
  return "", errors.New("No data")
 }
 jsonData, err := json.Marshal(data)
 return string(jsonData), err
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create a Strategy Builder

var (
 Err_NotSupportedExportType = errors.New("Not Supported export Type")
)

func UserExportBuilder(config *models.ExportConfig) (IUserExport, error) {
 if config.Type == enums.ExportType_CSV {
  return &CsvExportStrategy{}, nil
 } else if config.Type == enums.ExportType_Json {
  return &JSONExportStrategy{}, nil
 }
 return nil, fmt.Errorf("%w, %d", Err_NotSupportedExportType, config.Type)
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Update UserService 🥁

func (userSvc *UserService) ExportUsers(paginationObj models.Pagination, 
exportConfig exportstrategy.IUserExport) (string, error) {

 users := userSvc.GetUsers(paginationObj)
 return exportConfig.Export(users)
}
Enter fullscreen mode Exit fullscreen mode

Now, ExportUsers does not care how data is exported. It only retrieves users and sends the data to the correct export strategy.

Step 5: Adding a New Format (XML)

// XML Export: Concrete Strategy
type XMLExportStrategy struct {
}

func (e *XMLExportStrategy) Export(data []models.UserModel) (string, error) {
 if len(data) == 0 {
  return "", errors.New("No data")
 }
 xmlData, err := xml.Marshal(data)
 return string(xmlData), err
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Update the Strategy Builder

func UserExportBuilder(config *models.ExportConfig) (IUserExport, error) {
 switch config.Type {
 case enums.ExportType_CSV:
  return &CsvExportStrategy{}, nil
 case enums.ExportType_Json:
  return &JSONExportStrategy{}, nil
 case enums.ExportType_Xml:
  return &XMLExportStrategy{}, nil
 default:
  return nil, fmt.Errorf("%w, %d", Err_NotSupportedExportType, config.Type)
 }
}
Enter fullscreen mode Exit fullscreen mode

You can find this code on my Github: https://github.com/architagr/design_patterns/tree/main/golang/behavioral/strategy

🚀 Final Thoughts & Takeaways

Before: Every new format meant editing UserService, risking bugs.
After: Adding new formats is just creating a new strategy, with no changes to the core logic.
Note:
If your if-else statements are growing faster than your stress levels, it's time for the Strategy Pattern.

Stay Connected!

💡 Follow me on LinkedIn: Archit Agarwal 
🎥 Subscribe to my YouTube: The Exception Handler
 📬 Sign up for my newsletter: The Weekly Golang Journal
✍️ Follow me on Medium: @architagr
👨‍💻 Join my subreddit: r/GolangJournal

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (0)

Postmark Image

"Please fix this..."

Focus on creating stellar experiences without email headaches. Postmark's reliable API and detailed analytics make your transactional emails as polished as your product.

Start free

👋 Kindness is contagious

Dive into this thoughtful article, cherished within the supportive DEV Community. Coders of every background are encouraged to share and grow our collective expertise.

A genuine "thank you" can brighten someone’s day—drop your appreciation in the comments below!

On DEV, sharing knowledge smooths our journey and strengthens our community bonds. Found value here? A quick thank you to the author makes a big difference.

Okay