DEV Community

Puneet Khandelwal
Puneet Khandelwal

Posted on

Bounded Contexts Not Borders: Why Microservices Are a Misnomer

I've spent the last decade preaching microservices, but I was wrong. What we call microservices usually means a bunch of loosely coupled and independently deployable units that are actually a misnomer. We need to focus on true functional boundaries instead of arbitrary borders.

The problem starts with how we build them. We draw lines based on technical convenience, org charts, or vague requirements. Systems balloon with unnecessary complexity. Bounded contexts reflect the actual domain.

Take an e-commerce app. Order management, inventory tracking, and payment processing look like natural split points. Then they start talking to each other directly. Order management calls inventory, which checks payment status. You get a distributed monolith that is harder to debug than a clean single-binary app.

Here is a basic Go setup before splitting:

// monolithic.go
package main

import (
    "net/http"

    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()

    // Order management
    r.HandleFunc("/orders", handleOrders).Methods("GET")

    // Inventory tracking
    r.HandleFunc("/inventory", handleInventory).Methods("GET")

    // Payment processing
    r.HandleFunc("/payment", handlePayment).Methods("POST")

    http.ListenAndServe(":8080", r)
}
Enter fullscreen mode Exit fullscreen mode

Now look at what happens when we split by technical convenience:

// orderService.go
package main

import (
    "net/http"

    "github.com/gorilla/mux"
)

func handleOrders(w http.ResponseWriter, r *http.Request) {
    inventoryService := newInventoryService()
    levels, err := inventoryService.GetLevels()
    if err!= nil {
 http.Error(w, err.Error(), http.StatusInternalServerError)
 return
    }

    paymentService := newPaymentService()
    status, err := paymentService.GetStatus()
    if err!= nil {
 http.Error(w, err.Error(), http.StatusInternalServerError)
 return
    }

    w.Write([]byte(`{"orders": []}`))
}

// inventoryService.go
package main

func handleInventory(w http.ResponseWriter, r *http.Request) {
    paymentService := newPaymentService()
    status, err := paymentService.GetStatus()
    if err!= nil {
 http.Error(w, err.Error(), http.StatusInternalServerError)
 return
    }

    w.Write([]byte(`{"levels": []}`))
}

// paymentService.go
package main

func handlePayment(w http.ResponseWriter, r *http.Request) {
    orderService := newOrderService()
    status, err := orderService.GetStatus()
    if err!= nil {
 http.Error(w, err.Error(), http.StatusInternalServerError)
 return
    }

    w.Write([]byte(`{"status": "success"}`))
}
Enter fullscreen mode Exit fullscreen mode

Every service relies on the others. Coupling creeps back in.

Bounded contexts fix this by matching domain logic. Keep order management, inventory, and payments separate, but clean up the chatty network calls.

Focus on real domain boundaries. Systems become easier to scale and maintain when you stop carving arbitrary lines.

Top comments (0)