Integration patterns provide reusable solutions for connecting distributed systems.

Whether you’re building microservices, SaaS platforms, or cloud-native applications, you’ll often face challenges around data exchange, reliability, and scalability. Enterprise Integration Patterns (EIP) give you a toolkit to design robust communication between components.

Integration Patterns in Go

🔌 Core Integration Patterns

1. Point-to-Point

A direct connection between two systems.
Good for simplicity, but becomes a spaghetti mess as integrations grow (n² problem).

flowchart LR
    A[Service A] --> B[Service B]

🧑‍💻 Example (Go client calling API):

1
2
3
4
5
resp, err := http.Get("http://service-b:8080/data")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

✅ Simple, fast ⚠️ Tight coupling, doesn’t scale


2. Message Queue

A producer sends messages to a queue, and consumers process them asynchronously.

flowchart LR
    P[Producer] --> Q[(Queue)]
    Q --> C1[Consumer 1]
    Q --> C2[Consumer 2]

🧑‍💻 Example with RabbitMQ (using streadway/amqp):

1
2
3
4
ch, _ := conn.Channel()
q, _ := ch.QueueDeclare("tasks", false, false, false, false, nil)
ch.Publish("", q.Name, false, false,
    amqp.Publishing{ContentType: "text/plain", Body: []byte("task data")})

✅ Decouples producer & consumer ✅ Smooths traffic spikes, supports retry ⚠️ Adds latency, requires broker


3. Publish–Subscribe (Pub/Sub)

A publisher emits events to a broker; multiple subscribers consume independently.

flowchart LR
    P[Publisher] --> B[(Broker)]
    B --> S1[Subscriber 1]
    B --> S2[Subscriber 2]
    B --> S3[Subscriber 3]

🧑‍💻 Example with NATS:

1
2
3
4
5
6
7
8
nc, _ := nats.Connect(nats.DefaultURL)
defer nc.Drain()

nc.Subscribe("orders.created", func(m *nats.Msg) {
    fmt.Printf("Received: %s\n", string(m.Data))
})

nc.Publish("orders.created", []byte("Order#123"))

✅ Decouples producers/consumers ✅ Scales horizontally ⚠️ Delivery/order guarantees require tuning


4. Request–Reply

Classic synchronous API call. Go’s net/http or grpc are common implementations.

sequenceDiagram
    Client->>Service: Request
    Service-->>Client: Response

🧑‍💻 Example with gRPC:

1
2
3
4
5
conn, _ := grpc.Dial("service-b:50051", grpc.WithInsecure())
client := pb.NewUserServiceClient(conn)

resp, _ := client.GetUser(ctx, &pb.GetUserRequest{Id: "42"})
fmt.Println(resp.Name)

✅ Familiar, widely supported ⚠️ Tight coupling, fragile if callee is down


5. Event-Driven / Event Sourcing

State changes are represented as immutable events. Consumers react asynchronously.

flowchart LR
    A[Service A] -->|Event| E[(Event Log)]
    B[Service B] -->|Consume Event| E
    C[Service C] -->|Consume Event| E

🧑‍💻 Example: append events to Kafka

1
2
3
4
5
6
7
8
writer := kafka.NewWriter(kafka.WriterConfig{
    Brokers: []string{"localhost:9092"},
    Topic:   "user-events",
})

writer.WriteMessages(context.Background(),
    kafka.Message{Key: []byte("user-1"), Value: []byte("UserCreated")},
)

✅ Full audit log, replay possible ✅ Decoupled, scalable ⚠️ Requires careful schema/versioning strategy


🏗️ Advanced Architectural Patterns

1. CQRS (Command Query Responsibility Segregation) with Kafka

CQRS separates responsibilities into a write side (Commands) and a read side (Queries).
Instead of a single API serving both reads and writes, CQRS allows you to optimize each:

  • Writes (Commands): exposed as REST APIs for simple, synchronous commands.
  • Reads (Queries): exposed as GraphQL for flexible queries, or WebSockets for real-time updates.

In event-driven architectures, CQRS is often combined with Kafka:

  • the Write side publishes events,
  • the Read side consumes and projects them into optimized read models.
  • Microservices remain independent, but their state converges via eventual consistency.
flowchart LR
    ClientW[Client - REST Write] -->|POST /users| WriteAPI[Write Service]
    WriteAPI -->|UserCreated Event| K[(Kafka Topic)]
    
    K --> Projector[Read Model Projector]
    Projector --> ReadDB[(Read Database)]
    
    ReadDB --> GQL[GraphQL API]
    ReadDB --> WS[WebSocket Stream]
    
    ClientR1[Client - Query] -->|GraphQL Query| GQL
    ClientR2[Client - Realtime UI] -->|Subscribe| WS

🧑‍💻 Go Example – Write Side (REST Command API)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Write handler - create user
func (s *Server) CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    var user User
    json.NewDecoder(r.Body).Decode(&user)

    // Persist to write DB
    s.writeDB.Save(user)

    // Publish event to Kafka
    event := fmt.Sprintf(`{"event":"UserCreated","id":"%s","name":"%s"}`, user.ID, user.Name)
    s.kafkaWriter.WriteMessages(context.Background(),
        kafka.Message{Key: []byte(user.ID), Value: []byte(event)})

    w.WriteHeader(http.StatusCreated)
}

🧑‍💻 Go Example – Read Side (GraphQL + WebSocket)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// GraphQL resolver for querying users
func (r *Resolver) User(ctx context.Context, id string) (*User, error) {
    return r.readDB.GetUserByID(id)
}

// WebSocket broadcaster (pseudo-code)
func (s *Server) StreamUserEvents(ws *websocket.Conn) {
    for msg := range s.kafkaReader.C {
        ws.WriteMessage(websocket.TextMessage, msg.Value)
    }
}

✅ Benefits

  • REST commands are simple and predictable for writes.

  • GraphQL and WebSockets make reads flexible and real-time.

  • Microservices keep independent state but synchronize through events.

  • Highly scalable — read and write paths scale separately.

⚠️ Challenges

  • Eventual consistency: data in the read model may lag behind the write.

  • More moving parts: requires careful schema evolution, retries, and monitoring.

  • Debugging distributed state requires strong observability.

🌍 Real-world use case

Imagine a User Service with CQRS:

  • Writes (REST): POST /users creates a user and emits UserCreated.

  • Other microservices (Billing, Notifications) consume that event asynchronously and update their state.

  • Reads (GraphQL/WebSocket): A dashboard queries aggregated data (user + billing status) or subscribes to real-time updates without hitting the write database.

Over time, the system achieves eventual consistency: all services converge on the same user state, but they don’t have to be strongly consistent at write time.

👉 This now shows a CQRS microservices architecture where:

  • Writes = REST
  • Reads = GraphQL + WebSockets
  • State is shared asynchronously via Kafka with eventual consistency

2. Saga Pattern (Distributed Transactions)

In a microservices world, a single business process (e.g., “place order”) may span multiple services.
A Saga coordinates these steps to ensure eventual consistency without requiring a global transaction.

  • Choreography (event-based): each service listens for events and emits compensating events if something fails.
  • Orchestration (central coordinator): a Saga orchestrator tells each service what to do next and how to roll back if needed.
sequenceDiagram
    participant C as Customer Service
    participant O as Order Service
    participant P as Payment Service
    participant I as Inventory Service

    C->>O: Place Order
    O->>P: Reserve Payment
    P-->>O: Payment Reserved
    O->>I: Reserve Stock
    I-->>O: Stock Reserved
    O-->>C: Order Confirmed

Note over O,I,P: If any step fails → compensating events (e.g., Cancel Payment)

🧑‍💻 Example in Go (compensating event):

1
2
3
4
5
6
7
8
type OrderCancelled struct {
    OrderID string
    Reason  string
}

// If stock reservation fails
cancelEvent := OrderCancelled{OrderID: "123", Reason: "Out of stock"}
publish(cancelEvent)

✅ Benefits: Handles long-running, multi-service transactions without 2PC. ⚠️ Challenges: Requires careful design of compensating actions.


3. Circuit Breaker (Resilience)

A Circuit Breaker protects your system from cascading failures. When a dependency fails repeatedly, the breaker “opens” and stops calls until recovery is detected.

flowchart LR
    A[Service A] -->|Request| B[Service B]
    B -- success --> A
    B -- failure x N --> CB[Circuit Breaker]
    CB -.->|Fallback| A

🧑‍💻 Example with Go + resilience library (pseudo-code):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
    Name: "PaymentService",
})

result, err := cb.Execute(func() (interface{}, error) {
    return callPaymentService()
})
if err != nil {
    log.Println("Fallback: return cached response or error")
}

✅ Benefits: Prevents cascading failures, improves stability. ⚠️ Challenges: Needs good fallback strategies.


4. API Gateway / Aggregator

An API Gateway is a single entry point that routes requests to multiple microservices. Sometimes it also aggregates responses from multiple services to reduce client complexity.

flowchart LR
    Client --> Gateway[API Gateway]
    Gateway --> U[User Service]
    Gateway --> O[Order Service]
    Gateway --> P[Payment Service]

🧑‍💻 Example aggregator in Go (pseudo-code):

1
2
3
4
5
6
7
func AggregateOrderData(orderID string) OrderView {
    user := userService.GetUser(orderID)
    payment := paymentService.GetPayment(orderID)
    order := orderService.GetOrder(orderID)

    return OrderView{User: user, Order: order, Payment: payment}
}

✅ Benefits: Simplifies client logic, centralizes auth/routing. ⚠️ Challenges: Gateway can become a bottleneck if overloaded.


✅ Conclusion

Integration patterns are the glue of modern distributed systems.
Choosing the right one depends on your trade-offs and the maturity of your architecture:

  • Need simplicity → Point-to-Point / Request–Reply
  • Need resilience → Queues / Circuit Breaker
  • Need scalability → Pub/Sub / Event Sourcing
  • Need consistency in distributed workflows → Saga
  • Need optimized reads and writes → CQRS with Kafka
  • Need simplified client access → API Gateway / Aggregator

By applying these patterns in Go — with tools like gRPC, RabbitMQ, Kafka, NATS, Envoy, and GraphQL — you can build systems that are:

  • Scalable → handle traffic growth without bottlenecks
  • Fault-tolerant → recover gracefully from failures
  • Maintainable → clear separation of concerns and modular design
  • Future-proof → adaptable to new requirements as your system evolves

Integration patterns are not silver bullets, but they provide a toolbox of proven solutions. The key is knowing when to use which pattern — and combining them wisely.

Happy integrating! 🔗🐹


🚀 Follow me on norbix.dev for more insights on Go, Python, AI, system design, and engineering wisdom.