Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Every non-trivial system eventually hits the same wall: the data model that makes writes safe and consistent is a terrible fit for the queries users actually need. A normalized relational schema enforces referential integrity beautifully but forces expensive multi-table joins on every page load. Denormalizing for reads breaks write isolation. Adding indexes for query performance bloats write throughput. CQRS names this tension and resolves it structurally — not by tuning your way out of a design mismatch, but by acknowledging that reads and writes are different problems that deserve different models. Until you internalize this asymmetry, you will keep fighting your ORM instead of shipping.
A single User model forced to serve both a registration command and a dashboard query shows the tension immediately.
Use these three in order. Each builds on the one before.
In one paragraph, explain the read/write asymmetry in database-backed applications like I'm new to backend systems.
Walk me through exactly why a single ORM model forces every read path to pay the cost of the heaviest read — trace it from schema design through query planning to network bytes.
Given a social feed service where 95% of traffic is reads and writes must be consistent, how would the read/write asymmetry manifest at 10k req/s and what breaks first?
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
)
// One "god" struct trying to serve both reads and writes.
// Writes need Email + PasswordHash. Reads need DisplayName + PostCount.
// Neither side gets what it really wants.
type User struct {
ID int
Email string
PasswordHash string
DisplayName string
Bio string
PostCount int // computed — wrong to store here
FollowerCount int // computed — wrong to store here
}
func main() {
db, err := sql.Open("postgres", "host=localhost dbname=demo sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Write path: we only need Email + PasswordHash.
// But we SELECT the whole struct, pulling computed fields that don't exist yet.
var u User
err = db.QueryRow(
"SELECT id, email, password_hash, display_name, bio, (SELECT COUNT(*) FROM posts WHERE author_id = u.id) AS post_count, (SELECT COUNT(*) FROM follows WHERE followee_id = u.id) AS follower_count FROM users u WHERE id = $1", 1).
Scan(&u.ID, &u.Email, &u.PasswordHash, &u.DisplayName, &u.Bio,
&u.PostCount, &u.FollowerCount)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Loaded user %d — but we paid for two subqueries just to register them\n", u.ID)
// The write path should have been: INSERT INTO users(email, password_hash) VALUES($1,$2)
// The read path should have been: SELECT from a pre-joined view or Redis hash.
}go run main.go