{} The Go Reference

Arch structure · Architecture · Intermediate

Architecture Patterns (Styles)

A field guide to the macro architecture styles — layered, event-driven, microkernel/plugin, microservices, and space-based — what each optimizes for, and how to choose by trade-offs instead of fashion.

Arch structure Intermediate ⏱ 6 min read Complete

🏗️ Analogy

The pages around this one zoom in — how to model a domain, where the I/O seam goes. This one zooms out: the shape of the whole building. A bungalow (layered), a mall with independent shops (microservices), a power strip you plug modules into (microkernel), a trading floor where everyone shouts updates (event-driven), a fleet of identical food trucks that carry their own stock (space-based) — each is “good architecture” for a different job. Picking one is choosing what you optimize and what you sacrifice.

Why a deliberate style at all

Skip the decision and you drift into the big ball of mud: modules with no clear roles or dependency rules, everything tightly coupled, and nobody able to answer “does this scale? how does it deploy? how easily does it change?” without reading the whole codebase. A named architecture style gives you those answers up front. The five below are the classic macro patterns catalogued in Mark Richards’ Software Architecture Patterns (O’Reilly, free report) — the canonical short read for this material; this page is the Go-flavored tour.

The five styles at a glance

graph TD
Layered["① Layered (n-tier)<br/>presentation→business→persistence→db"]
Event["② Event-Driven<br/>decoupled processors react to events"]
Micro["③ Microkernel<br/>small core + plug-ins"]
Services["④ Microservices<br/>independently deployable services"]
Space["⑤ Space-Based<br/>in-memory grid, no central DB"]

① Layered (n-tier). Horizontal layers — presentation, business, persistence, database — each a closed abstraction. A request passes through each layer (you can’t skip one), giving layers of isolation: a change in one layer doesn’t ripple to others. Simple, familiar, the default for most apps. Weaknesses: it scales and deploys as one unit, and risks the architecture sinkhole (requests that fall straight through every layer doing nothing but forwarding). Use when: small-to-medium apps, you want the lowest-ceremony start. (Closely related to the modular monolith.)

② Event-Driven. Decoupled event processors that react to events asynchronously. Two topologies: a mediator orchestrates a known multi-step workflow centrally; a broker has no orchestrator — events chain reactively from one processor to the next (more decoupled, harder to trace/recover). Use when: highly responsive, asynchronous, spiky workloads; complex event flows. (In Go this is channels, pub/sub, and message queues.)

③ Microkernel (plug-in). A minimal core plus independent plug-in modules the core discovers and invokes through a registry/contract. Use when: product-style apps with a stable base and lots of optional or customer-specific features — IDEs, browsers, rules/task engines.

④ Microservices. Independently deployable, separately scalable services around bounded contexts. Use when: a real force (independent deploy/scale, team autonomy) justifies the distributed-systems tax — covered in depth in modular monolith vs microservices and microservices basics.

⑤ Space-Based. Removes the central-database bottleneck: processing units hold data in memory and share it through a replicated tuple space (in-memory data grid), with a backing store updated asynchronously. Use when: extreme, unpredictable scalability and high concurrency (think flash-sale / ticketing spikes) — at the cost of real complexity and eventual consistency.

See it: the microkernel core + plug-in registry

The microkernel pattern is tiny and clean in Go: a core holds a registry of plug-ins satisfying a contract interface, and dispatches to them without knowing what they do. Adding a feature means registering a plug-in — the core never changes. This runs here:

microkernel.go — editable & runnable
package main

import (
"fmt"
"sort"
)

// Plugin is the contract the core knows about — the only coupling.
type Plugin interface {
Name() string
Apply(order float64) float64
}

// --- core: a minimal engine that discovers & invokes plug-ins ---
type Core struct{ plugins map[string]Plugin }

func NewCore() *Core { return &Core{plugins: map[string]Plugin{}} }

func (c *Core) Register(p Plugin) { c.plugins[p.Name()] = p }

func (c *Core) Process(order float64, active []string) float64 {
sort.Strings(active) // deterministic order
for _, name := range active {
	if p, ok := c.plugins[name]; ok {
		order = p.Apply(order)
	}
}
return order
}

// --- independent plug-ins: the core has zero knowledge of these ---
type loyalty struct{}
func (loyalty) Name() string             { return "loyalty" }
func (loyalty) Apply(o float64) float64  { return o * 0.95 } // 5% off

type tax struct{}
func (tax) Name() string                 { return "tax" }
func (tax) Apply(o float64) float64      { return o * 1.10 } // +10%

func main() {
core := NewCore()
core.Register(loyalty{}) // add a feature = register a plug-in;
core.Register(tax{})     // the core code never changes.

total := core.Process(100, []string{"loyalty", "tax"})
fmt.Printf("final: %.2f\n", total) // loyalty then tax: 100*0.95*1.10
}

The Core depends only on the Plugin interface — exactly the dependency-inversion idea from hexagonal architecture, applied at the scale of whole features. New behavior plugs in; the core stays stable.

Choosing: it’s all trade-offs

There is no “best” style — each optimizes some qualities at the expense of others:

StyleScalesDeploys independentlySimplicityBest for
Layeredwhole appno★★★most CRUD / line-of-business apps
Event-drivenhighpartlyasync, spiky, reactive flows
Microkernellow–medplug-ins★★extensible products
Microservicesper serviceyesindependent deploy/scale/teams
Space-basedextremeyesunpredictable high-concurrency spikes

🐹 Most Go systems start layered/modular — and that's right

For the typical Go service, a clean modular monolith (a disciplined layered/feature-sliced design) is the correct first style: simple to build, run, and reason about. Reach for the others when a specific quality need appears — event-driven when you need asynchronous, decoupled reactions (Go’s channels and queues make it natural); microkernel when you’re building an extensible product; microservices when independent deploy/scale/teams justify the cost; space-based only for genuinely extreme, spiky scale. The architect’s job, per Richards, is to justify the choice against business and quality needs — never to adopt a style because it’s fashionable.

⚠️ Styles compose — and the labels blur

Real systems mix these. A microservice is usually internally layered (or hexagonal); an event-driven system often has a microkernel-style processor; a space-based system communicates via events. Don’t treat the five as mutually exclusive boxes — treat them as a vocabulary of forces. The mistake isn’t combining them; it’s adopting the heaviest one (microservices, space-based) for prestige before any quality need demands it, and paying enormous complexity for benefits you don’t use — the flip side of the big ball of mud.

See also

Back to the Architecture index, or explore the Cloud-Native track.

Check your understanding

Score: 0 / 6

1. What is the 'big ball of mud' anti-pattern that motivates having a deliberate architecture?

The 'big ball of mud' is what you get by coding without a deliberate architecture: source modules with no clear roles, responsibilities, or dependency rules, so everything is tightly coupled and brittle. Basic questions — does it scale? how does it deploy? how easily does it change? — become impossible to answer without understanding every component. Choosing an architecture style up front is how you avoid sliding into the mud.

2. In a classic layered (n-tier) architecture with CLOSED layers, what is 'layers of isolation'?

Closed layers mean a request flows top-to-bottom through each layer (presentation → business → persistence → database); you can't skip one. 'Layers of isolation' is the payoff: because each layer talks only to the contract of the layer directly below, a change confined to one layer (e.g. swapping the UI framework) doesn't affect the others. The cost is the 'architecture sinkhole' risk — requests that just pass straight through every layer doing nothing but forwarding.

3. What distinguishes the two event-driven topologies, mediator and broker?

Event-driven architecture is built from decoupled event processors. In the MEDIATOR topology a central mediator receives an event and orchestrates the steps (knowing the workflow), good for events needing coordination. In the BROKER topology there's no central orchestrator — a processor publishes an event and other processors react and publish their own, forming a chain. Broker is more decoupled and scalable but the overall flow is harder to follow and to recover when a step fails.

4. What is the core idea of the microkernel (plug-in) architecture?

Microkernel architecture splits the system into a small, stable core (the minimum to make it run) plus plug-in modules that add specialized or customer-specific behavior. The core knows how to discover and call plug-ins through a registry/contract, but plug-ins are independent and ideally don't know about each other. It's ideal for product-style apps with a stable base and lots of optional/extensible features (IDEs, browsers with extensions, task/rules engines).

5. What problem does the space-based architecture specifically target?

Space-based architecture (a.k.a. cloud/grid architecture) attacks the classic scalability killer: the database becoming the bottleneck under high, variable load. Instead, processing units hold data in-memory and share/replicate it through a 'tuple space' (an in-memory data grid), with a backing store updated asynchronously. By eliminating the central DB from the request path, it scales out elastically for spiky, high-concurrency workloads — at the cost of significant complexity and eventual-consistency concerns.

6. What's the right way to CHOOSE among these architecture styles?

Every architecture style is a bundle of trade-offs across qualities like agility, ease of deployment, testability, performance, scalability, and overall simplicity. Layered is simple but scales and deploys poorly; microservices deploy and scale independently but add operational complexity; space-based scales extremely but is complex; microkernel is great for extensibility but not for raw scale. An architect's job is to justify the choice against the specific business and quality needs — not to follow fashion.

Comments

Sign in with GitHub to join the discussion.