Go architectures · 3 MIN READ

Modular monolith: boundaries inside one deployment

Give business capabilities explicit APIs while retaining local transactions.

Give business capabilities explicit APIs while retaining local transactions. A modular monolith groups capabilities such as billing and shipping behind package APIs. Trace the worked Go example, then compare the benefit with the cost before applying this design to your own project.

Architecture · A MAP OF THE IDEA

  1. Composition root wires modules
  2. Shipping calls a billing query
  3. Billing keeps its state private

Useful when

  • Business boundaries are clearer than infrastructure boundaries
  • One team wants local calls and coordinated transactions

Watch out

  • One deployment still couples release timing
  • Shared tables can erode module ownership

Implementation & operation cost: Medium

Fit depends on the task

Small tool or CLI3/5
Domain-rich application5/5
Independent distributed services3/5

Editorial suitability, not benchmarks or a universal ranking. Styles describe different axes and can be combined.

01

The design decision

A modular monolith groups capabilities such as billing and shipping behind package APIs. Keep their implementation and tables private by convention and review. A module is not an excuse for another global models package that every feature can mutate.

02

A complete Go example

Read this program from the entry point and follow the calls shown in the diagram. The output is included at the end so you can check the behavior without a compiler.

go
package main

import (
	"fmt"
)

type Billing struct{ paid map[string]bool }

func (b Billing) IsPaid(id string) bool { return b.paid[id] }

type Shipping struct{ paid func(string) bool }

func (s Shipping) CanShip(id string) bool { return s.paid(id) }

func main() {
	billing := Billing{paid: map[string]bool{"order-1": true}}
	shipping := Shipping{paid: billing.IsPaid}
	fmt.Println(shipping.CanShip("order-1"))
	fmt.Println(shipping.CanShip("order-2"))
}

// Output:
// true
// false
Read the code

Shipping receives the one query it needs instead of the billing map. Both calls stay in the same process. In a real module layout these declarations belong in separate packages, with internal implementation hidden behind each module API.

03

Tradeoffs and the simpler alternative

One database can support atomic changes, but bypassing module APIs destroys ownership. Enforce dependency direction with package boundaries and review. Extract a service only when independent scaling, release cadence or isolation pays for remote failures.

04

Turn capability ownership into package APIs

Billing can expose IsPaid or ConfirmPayment without exporting its payment map or SQL tables. Shipping asks the billing API instead of reading billing tables directly. A practical layout puts each capability below internal with a small exported surface and keeps application wiring in cmd. Go internal restricts outside imports but does not prevent sibling packages from bypassing ownership, so unexport implementation types and review dependencies. Sharing one database is an operational choice, not permission for every module to mutate every table.

05

Choose consistency across module boundaries

When two changes truly require one atomic transaction, define the coordinator and transaction-scoped operations explicitly. When they can be eventually consistent, commit the initiating change and an outbox record together, then apply the receiving action idempotently. Document what the user sees while shipping has not yet observed payment. An in-memory notification after commit can be lost if the process crashes, so it is not a substitute for a durable handoff.

06

Extract without breaking every caller

A module is ready for service extraction when its contract, ownership and data access are already explicit. First find direct table reads, shared pointers and cross-module transactions; these are migration work. Put a network adapter behind the existing consumer contract and introduce timeouts, versioning and retry-safe operations. Run contract tests before switching traffic. Keep the monolith if the same team deploys everything together and local transactions still provide more value than independent release timing.

TAKE THIS WITH YOU

Business capabilities expose APIs in one deployment. One database can support atomic changes, but bypassing module APIs destroys ownership. Enforce dependency direction with package boundaries and review. Extract a service only when independent scaling, release cadence or isolation pays for remote failures.

Check your understanding

Which statement best describes Modular monolith: boundaries inside one deployment?

Go to the source

Progress stays in this browser. No account needed.