Boost Order Processing with Go’s Concurrent Observer Pattern – A Step‑by‑Step Guide

This article explains the Observer design pattern, demonstrates how to implement it in Go using channels and goroutines for an order processing system, provides full code examples for subjects, observers, and a main routine, and compares it with a tightly‑coupled alternative.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
Boost Order Processing with Go’s Concurrent Observer Pattern – A Step‑by‑Step Guide

When building an order‑processing system, the Observer design pattern can be used to handle order‑status changes and notifications. This article shows how to implement the pattern in Go and provides a complete code example.

What Is the Observer Design Pattern?

The Observer pattern is a behavioral design pattern that enables loosely‑coupled communication between objects. A subject maintains a list of observers and automatically notifies them when its state changes, facilitating event‑driven architectures.

Application Scenario: Order Processing System

In an order system, statuses such as Created, Paid, Shipped, or Cancelled may change. When a status changes, relevant observers (e.g., inventory updater, notification sender) need to be informed to perform appropriate actions.

Implementing the Observer Pattern in Go

Go’s channels and goroutines can be leveraged to realize the pattern.

First, define the order structure:

type Order struct {
  ID     string
  Status string
}

Define the observer interface:

type Observer interface {
  Update(order *Order, wg *sync.WaitGroup)
}

Define the subject that registers observers and notifies them:

type Subject struct {
  observers []Observer
}

func (s *Subject) Register(observer Observer) {
  s.observers = append(s.observers, observer)
}

func (s *Subject) Notify(order *Order) {
  wg := sync.WaitGroup{}
  wg.Add(len(s.observers))

  errCh := make(chan error, len(s.observers))

  for _, observer := range s.observers {
    go func(obs Observer) {
      defer wg.Done()
      err := obs.Update(order, &wg)
      if err != nil {
        errCh <- err
      }
    }(observer)
  }

  wg.Wait()
  close(errCh)

  // handle errors
  for err := range errCh {
    fmt.Println("Error occurred:", err)
  }
}

The notification channel errCh collects any errors from observers; after all goroutines finish, the channel is closed and errors are processed.

Concrete Observers

Inventory observer updates stock status:

type InventoryObserver struct{}

func (io *InventoryObserver) Update(order *Order, wg *sync.WaitGroup) {
  defer wg.Done()
  // update inventory
  fmt.Printf("Inventory Observer: Order %s status changed to %s
", order.ID, order.Status)
}

Notification observer sends a notification:

type NotificationObserver struct{}

func (no *NotificationObserver) Update(order *Order, wg *sync.WaitGroup) {
  defer wg.Done()
  // send notification
  fmt.Printf("Notification Observer: Order %s status changed to %s
", order.ID, order.Status)
}

Main Function

func main() {
  order := &Order{ID: "123", Status: "Created"}
  subject := &Subject{}
  subject.Register(&InventoryObserver{})
  subject.Register(&NotificationObserver{})

  // simulate status changes
  order.Status = "Paid"
  subject.Notify(order)

  order.Status = "Shipped"
  subject.Notify(order)
}

By creating an order and a subject, registering observers, and invoking Notify after each status change, the system concurrently informs all observers, improving performance and responsiveness.

Advantages of Using the Observer Pattern

Loose coupling – observers depend only on the subject’s state, not its implementation.

Reusability – observers can be reused across different contexts.

Easy extensibility – new observers can be added without modifying existing code.

Event‑driven – ideal for systems that react to state changes.

What Happens Without the Observer Pattern?

Without the pattern, the order logic becomes tightly coupled. The following example shows a naïve implementation:

type Order struct {
  ID     string
  Status string
}

type OrderProcessor struct {
  inventoryObserver    *InventoryObserver
  notificationObserver *NotificationObserver
}

func NewOrderProcessor() *OrderProcessor {
  return &OrderProcessor{inventoryObserver: &InventoryObserver{}, notificationObserver: &NotificationObserver{}}
}

func (op *OrderProcessor) Process(order *Order) {
  op.inventoryObserver.Update(order)
  op.notificationObserver.Update(order)
}

func main() {
  order := &Order{ID: "123", Status: "Created"}
  op := NewOrderProcessor()
  order.Status = "Paid"
  op.Process(order)
  order.Status = "Shipped"
  op.Process(order)
}

In this design, OrderProcessor directly depends on concrete observer types, leading to tight coupling, code duplication, and poor extensibility.

By contrast, the Observer pattern decouples components, reduces duplication, and makes the system easier to maintain and extend.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design PatternsBackend DevelopmentconcurrencyObserver Pattern
MaGe Linux Operations
Written by

MaGe Linux Operations

Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.