GORM — Architecture#
Architectural style#
Layered Library with Callback Pipeline and Dialect Plugin System
GORM is a pure library (no binary, no server). Its architecture has three distinct layers:
- Public API layer — the root
gormpackage exports the user-facing API (DB,Config,Statement, chainable + finisher methods). This is what library consumers import. - Callback pipeline layer — the
callbacks/package implements CRUD operations as ordered, composable function pipelines registered into per-operationprocessorobjects. This is the primary behavioral extension mechanism. - Domain sub-packages —
clause/(typed SQL AST),schema/(Go struct reflection),logger/,migrator/,utils/are pure leaf packages with no dependency on each other or oncallbacks/.
Cross-cutting the layering is the Dialector plugin system: the Dialector interface abstracts all database-specific behavior. External dialect packages (e.g., gorm.io/driver/postgres) implement Dialector and call callbacks.RegisterDefaultCallbacks() during Initialize(*DB), wiring the concrete CRUD implementations into the callback pipeline. This means GORM core compiles with zero database driver dependencies.
Evidence: gorm.go imports only clause/, logger/, and schema/ — no database/sql driver. interfaces.go defines Dialector as an 8-method interface. callbacks/callbacks.go:RegisterDefaultCallbacks() is called by dialects, not by the core.
Component diagram (textual)#
┌─────────────────────────────────────────────────────────────┐
│ User Code │
│ db.Where("name=?","Alice").Find(&users) │
└───────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────────┐
│ Root Package (gorm) │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────┐ │
│ │ DB struct │ │ Statement │ │ Config / Option │ │
│ │ (clone int) │ │ (SQL builder) │ │ (Plugin map) │ │
│ └──────┬───────┘ └──────┬────────┘ └──────────────────┘ │
│ │ chainable_api │ finisher_api │
│ └────────────┬────┘ │
└──────────────────────┼──────────────────────────────────────┘
│ Execute(db)
┌──────────────────────▼──────────────────────────────────────┐
│ Callback Pipeline (callbacks.processor) │
│ per operation: create / query / update / delete / row / raw│
│ │
│ [BeginTx] → [BeforeCreate] → [SaveAssoc] → [Create] │
│ → [SaveAssoc] → [AfterCreate] → [Commit/Rollback] │
│ │
│ Registered by: Dialector.Initialize() → RegisterDefaultCallbacks()│
└───────────┬───────────────────────┬────────────────────────┘
│ │
┌───────────▼──────────┐ ┌────────▼────────────────────────┐
│ clause/ (SQL AST) │ │ schema/ (Struct Reflection) │
│ SELECT, WHERE, │ │ Schema, Field, Relationship │
│ ORDER BY, … │ │ NamingStrategy, Serializer │
│ Dialect-agnostic │ │ Tag parsing, index/FK defs │
└───────────┬──────────┘ └────────────────────────────────┘
│ Dialector renders clauses
┌───────────▼──────────────────────────────────────────────┐
│ Dialector interface (e.g., gorm.io/driver/postgres) │
│ Name(), Initialize(), Migrator(), DataTypeOf(), │
│ BindVarTo(), QuoteTo(), Explain() │
└───────────┬──────────────────────────────────────────────┘
│
┌───────────▼──────────────────────────────────────────────┐
│ ConnPool interface (wraps database/sql) │
│ ExecContext / QueryContext / QueryRowContext │
│ PreparedStmtDB (optional LRU cache layer) │
└──────────────────────────────────────────────────────────┘Core components#
DB#
- Package:
gorm.io/gorm(root) - Responsibility: The user-facing database handle. Carries a pointer to
Config(shared) and a*Statement(per-operation). Theclone intfield implements a lazy copy-on-write strategy:clone=1means the next operation gets a freshStatement;clone=2clones the existing one. - Key types:
DB,Session,Config - Dependencies:
clause/,logger/,schema/,Statement,callbacks
Statement#
- Package:
gorm.io/gorm(statement.go) - Responsibility: Mutable scratchpad for a single database operation. Holds the clause map, SQL builder, parameter vars, model/dest values, schema, connection pool reference, and per-query settings. Assembled by the chainable API and consumed by the callback pipeline.
- Key types:
Statement,join,StatementModifier - Dependencies:
clause/,schema/,ConnPool
callbacks / processor#
- Package:
gorm.io/gorm(callbacks.go) +gorm.io/gorm/callbacks - Responsibility: The behavioral core.
callbacksmanages six namedprocessorobjects (create/query/update/delete/row/raw). Eachprocessorholds an ordered slice offunc(*DB)handlers compiled from declarativebefore/afterconstraints.processor.Execute(db)runs all registered fns in order. - Key types:
callbacks,processor,callback - Dependencies: Root package,
schema/,utils/
clause (SQL AST)#
- Package:
gorm.io/gorm/clause - Responsibility: Typed representation of every SQL clause (SELECT, FROM, WHERE, JOIN, ORDER BY, LIMIT, GROUP BY, ON CONFLICT, RETURNING, WITH, FOR). Each clause type implements
Expression.Build(*Statement)to render dialect-specific SQL via theDialector.QuoteTo/BindVarTohooks. Provides safe, composable query construction without string concatenation. - Key types:
Clause,ClauseBuilder,Expression,Writer,Expr,Column,Table - Dependencies: None (leaf package)
schema#
- Package:
gorm.io/gorm/schema - Responsibility: Reflects Go struct types into a rich schema model: table name, columns, data types, relationships (HasOne/HasMany/BelongsTo/ManyToMany), constraints, indexes, and serializers. Results are cached in a
sync.Mappool to avoid repeated reflection. Implements theNamingStrategyinterface for table/column naming conventions. - Key types:
Schema,Field,Relationship,NamingStrategy,Serializer - Dependencies:
jinzhu/inflection,jinzhu/now,golang.org/x/text
Dialector (interface / adapter)#
- Package: defined in
gorm.io/gorm/interfaces.go, implemented in separate driver repos - Responsibility: Encapsulates all database-specific behavior: binding variables, quoting identifiers, explaining SQL, providing the migrator implementation, mapping Go types to DB types. During
Initialize(*DB), the dialector callscallbacks.RegisterDefaultCallbacks()to wire CRUD implementations. - Key types:
Dialectorinterface (8 methods) - Dependencies: Root package
*DB,clause/,schema/
logger#
- Package:
gorm.io/gorm/logger - Responsibility: Logging abstraction with configurable log levels, slow-query threshold, SQL parameter redaction, and elapsed-time reporting. Ships both a default colorized implementation and a Go 1.21
slog-based adapter. - Key types:
Interface,Config,Logger,SlogLogger - Dependencies: stdlib only
migrator#
- Package:
gorm.io/gorm/migrator - Responsibility: Portable migration logic (create table, add column, drop index, etc.) shared by all dialects as a base
CommonMigrator. Each dialect embeds this and overrides database-specific behavior. - Key types:
Migrator,ColumnType,TableType - Dependencies: Root package
Data flow#
Example: db.Where("name = ?", "Alice").First(&user)
Chainable phase —
Where("name = ?", "Alice"):db.getInstance()— ifclone > 0, allocates a newStatementwith fresh clause map (copy-on-write)- Adds a
clause.Where{Exprs: [clause.Expr{SQL: "name = ?", Vars: ["Alice"]}]}toStatement.Clauses - Returns the new
*DB(original unchanged)
Finisher phase —
First(&user):db.getInstance()— another copy- Sets
Statement.Dest = &user,Statement.Model = &user - Adds
clause.OrderBy,clause.Limit{Limit: 1}to Statement.Clauses - Calls
tx.callbacks.Query().Execute(tx)
Processor.Execute(db):
- Resolves any pending
scopes - Calls
stmt.Parse(stmt.Model)→schema/reflects theUserstruct, caches*schema.Schema - Iterates
p.fns(the ordered callback slice):[Query, Preload, AfterQuery]
- Resolves any pending
Query callback (
callbacks/query.go):- Calls
stmt.Build("SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "LIMIT", "FOR")— each clause renders itself intostmt.SQLviaclause.Expression.Build(stmt) Dialector.BindVarTo()andQuoteTo()inject database-specific syntax- Calls
stmt.ConnPool.QueryContext(ctx, stmt.SQL.String(), stmt.Vars...)→ returns*sql.Rows scan.gomaps column values back to theUserstruct fields viaschema.Fieldreflection
- Calls
AfterQuery callback — invokes the model’s
AfterFindhook if implementedLogging —
processor.Execute()callsdb.Logger.Trace()with SQL + elapsed time after all fns run
Transaction flow (Create with default transaction):
BeginTransaction → BeforeCreate → SaveBeforeAssociations
→ Create (SQL exec) → SaveAfterAssociations → AfterCreate
→ CommitOrRollbackBeginTransaction and CommitOrRollback are registered with a match predicate (!SkipDefaultTransaction) so they’re excluded when transactions are disabled.
Initialization / Bootstrap#
gorm.Open(dialector, opts...) in gorm.go:134:
- Option sorting —
*Configoptions are sorted to the front (applied first) - Option application —
opt.Apply(config)for each non-nil option (modifiesConfigin place) - Defaults — NamingStrategy, Logger, NowFunc, Plugins map, cacheStore are set if nil
- DB construction —
db = &DB{Config: config, clone: 1} - Callback initialization —
initializeCallbacks(db)creates 6 emptyprocessorobjects (no fns yet) - Dialector initialization —
config.Dialector.Initialize(db):- Dialect opens the underlying
*sql.DB - Sets
db.ConnPool - Calls
callbacks.RegisterDefaultCallbacks(db, config)— this is where CRUD implementations are registered into the processors
- Dialect opens the underlying
- PreparedStmt (if enabled) — wraps
ConnPoolinPreparedStmtDB(LRU-backed cache) - Root Statement —
db.Statement = &Statement{DB: db, ConnPool: ..., Context: context.Background(), Clauses: map[string]clause.Clause{}} - Ping — optional connection health check
- AfterInitialize — each option’s
AfterInitialize(db)runs (plugins are initialized here)
No DI framework is used. Dependencies are wired manually: DB embeds *Config (which holds the Dialector, callbacks, Logger, ConnPool). There is no IoC container. The Option interface (Apply + AfterInitialize) is GORM’s own lightweight equivalent of functional options with deferred post-init hooks.
Configuration#
Configuration uses the Option interface pattern — not purely functional options, but a two-phase interface:
Apply(*Config) error— modifies the Config struct before DB is createdAfterInitialize(*DB) error— runs after DB is fully initialized (used by plugins)
*Config itself implements Option, so passing a pre-built Config is the primary way to configure GORM:
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
NamingStrategy: schema.NamingStrategy{TablePrefix: "t_"},
PrepareStmt: true,
SkipDefaultTransaction: true,
})Per-session overrides use db.Session(&Session{...}) — this creates a shallow copy of Config with the session fields applied, leaving the original DB unmodified. db.WithContext(ctx) is sugar over db.Session(&Session{Context: ctx}).
The cacheStore (*sync.Map) allows arbitrary key-value storage on the DB instance, used internally for the prepared statement cache and available to plugins via db.Set / db.Get.
Key design decisions#
Callback pipeline as the extension mechanism, not inheritance. CRUD operations are not implemented as methods on
DB. Instead, they are registeredfunc(*DB)handlers into per-operation processors. Dialects register their implementations duringInitialize(). Users extend or override behavior by callingdb.Callback().Create().Before("gorm:create").Register("myplugin:hook", fn). This allows dialects, plugins, and user code to modify any stage of any operation without subclassing.Typed SQL AST (
clause/) instead of string concatenation. Every query is assembled from typed clause structs (SELECT, WHERE, ORDER BY, …) that implementExpression.Build(*Statement). TheStatementcarries amap[string]clause.Clausekeyed by clause name, so any stage can add or override clauses before rendering. Dialect-specific rendering (quoting, binding) is injected viaDialectormethods called duringBuild(). This makes query construction both composable and dialect-safe.DBclone / copy-on-write for chaining safety. Theclone intfield (0=no clone,1=new Statement,2=clone Statement) controls whethergetInstance()allocates a fresh or copied Statement. This allows chainable methods to be called on a shared*DBwithout race conditions — each chain starts from a cloned instance. The pattern is unusual in Go ORMs and trades some memory allocation cost for a clean chaining API.Dialectorinterface decouples core from all database drivers. The coregorm.io/gormmodule has zero SQL driver imports. Each dialect is a separate Go module. The interface boundary is rich enough (Initialize, Migrator, DataTypeOf, DefaultValueOf, BindVarTo, QuoteTo, Explain) to support full dialect customization, including custom type mapping, upsert syntax, and schema migration.schema/as a cached reflection layer. Rather than reflecting struct types on every query,schema.Parse()returns a cached*Schemafrom async.Mappool keyed by reflect.Type. The schema model captures relationships, serializers, constraints, and indexes at parse time. This amortizes reflection cost over the application lifetime and makes query execution reflection-free for warm paths.