Pop — Architecture#
Architectural style#
Layered Library with Dialect Plugin System
Pop is a library, not an application, so there is no long-running process or server. Its architecture is a shallow three-layer stack:
- User-facing API layer —
Connection,Query, and the free functions on them (Find,Save,Where, etc.) form the public surface. - Abstraction layer — two internal interfaces (
dialectandstore) decouple the ORM logic from both the underlying SQL driver and from database-specific SQL generation. - Dialect/driver layer — five concrete dialect implementations (
postgresql,mysql,mariadb,sqlite,cockroach) satisfy thedialectinterface and translate generic operations into database-specific SQL.
The soda CLI is a thin wrapper that bootstraps the library and delegates entirely to its public API.
Component diagram (textual)#
┌─────────────────────────────────────────────────────────────┐
│ soda CLI │
│ (soda/cmd — Cobra commands: create, drop, migrate, gen) │
└──────────────────────────┬──────────────────────────────────┘
│ imports pop public API
┌──────────────────────────▼──────────────────────────────────┐
│ Pop Library (root package) │
│ │
│ ┌──────────────────┐ ┌────────────────────────────────┐ │
│ │ Config system │ │ Connection │ │
│ │ (config.go, │──▶│ - Store store │ │
│ │ database.yml, │ │ - Dialect dialect │ │
│ │ envy env vars) │ │ - TX *Tx │ │
│ └──────────────────┘ └──────────┬───────────┬─────────┘ │
│ │ │ │
│ ┌─────────▼──┐ ┌────▼──────────┐ │
│ │ Query │ │ dialect i/f │ │
│ │ (builder) │ │ crudable │ │
│ │ .Where() │ │ fizzable │ │
│ │ .Order() │ │ quotable │ │
│ │ .Limit() │ └──────┬────────┘ │
│ │ .ToSQL() │ │ │
│ └─────┬──────┘ ┌─────▼──────────┐│
│ │ │ PostgreSQL ││
│ ┌─────▼──────┐ │ MySQL ││
│ │ store i/f │ │ MariaDB ││
│ │ (sqlx │ │ SQLite ││
│ │ wrapper) │ │ CockroachDB ││
│ └─────┬──────┘ └───────────────┘│
│ │ │
│ ┌────────────┐ ┌──────────┐ │ ┌──────────┐ ┌─────────┐ │
│ │ Model │ │ Migrator │ │ │Assoc'ns │ │Columns │ │
│ │(reflection)│ │(file- │ │ │(sub-pkg) │ │(sub-pkg)│ │
│ └────────────┘ │ based) │ │ └──────────┘ └─────────┘ │
│ └──────────┘ │ │
└────────────────────────────────┼────────────────────────────┘
│
┌──────▼──────┐
│ sqlx/sql │
│ (stdlib + │
│ jmoiron) │
└─────────────┘Core components#
Connection#
- Package:
github.com/gobuffalo/pop/v6(connection.go) - Responsibility: Central user-facing handle for all database interaction. Holds references to the active
store(sqlx connection or transaction) and thedialect(database-specific behavior). All CRUD operations and query building originate from or flow throughConnection. - Key types:
Connectionstruct (Store store,Dialect dialect,TX *Tx) - Dependencies:
storeinterface,dialectinterface,internal/randx,internal/defaults,logging
dialect (interface + implementations)#
- Package:
github.com/gobuffalo/pop/v6(dialect.go,dialect_*.go) - Responsibility: Abstracts all database-specific behavior. Composed of three sub-interfaces:
crudable(SELECT/INSERT/UPDATE/DELETE),fizzable(fizz DSL translation),quotable(identifier quoting). Implementations also handleCreateDB,DropDB,DumpSchema,LoadSchema, andTruncateAll. - Key types:
dialect(private interface),crudable,fizzable,quotable - Dependencies:
gobuffalo/fizz(translator),columnssub-package,database/sql
store (interface + implementations)#
- Package:
github.com/gobuffalo/pop/v6(store.go,db.go) - Responsibility: Thin abstraction over sqlx’s
*sqlx.DBand*sqlx.Tx. Allows context propagation (viacontextStorewrapper) without changing the coreConnectionAPI. Enables the instrumented driver overlay for tracing (Open Tracing, AWS X-Ray, etc.). - Key types:
store(private interface),dB(wraps sqlx.DB),contextStore(wraps store + ctx),Tx - Dependencies:
jmoiron/sqlx,database/sql,context
Query#
- Package:
github.com/gobuffalo/pop/v6(query.go,clause.go, plusfinders.go,executors.go,group.go,having.go,join.go,order.go,paginator.go) - Responsibility: Fluent query builder. Accumulates clauses (WHERE, ORDER, LIMIT, JOIN, GROUP BY, HAVING) and generates SQL via
ToSQL(model). Delegates execution to theConnection.Storeand dialect-specific CRUD methods. - Key types:
Querystruct,clause,clauses,operation(SELECT / DELETE) - Dependencies:
Connection,Model,associations,logging
Model#
- Package:
github.com/gobuffalo/pop/v6(model.go) - Responsibility: Reflection wrapper around user-provided structs. Derives table names (using
gobuffalo/flectfor pluralization), reads/writesID, managescreated_at/updated_attimestamps, and checks for optional interfaces (TableNameAble,TableNameAbleWithContext). - Key types:
Modelstruct,Value(interface{}),TableNameAble,TableNameAbleWithContext - Dependencies:
gobuffalo/flect,columns,gofrs/uuid
Migrator / FileMigrator#
- Package:
github.com/gobuffalo/pop/v6(migrator.go,file_migrator.go) - Responsibility: Tracks and runs database migrations.
FileMigratorreads.fizzor.sqlmigration files from disk, translates Fizz DSL throughdialect.FizzTranslator(), records applied migrations in aschema_migrationtable, and supports up/down/status/reset operations. - Key types:
Migrator,Migration,FileMigrator - Dependencies:
dialect(for fizz translation and schema ops),Connection,gobuffalo/fizz
Config system#
- Package:
github.com/gobuffalo/pop/v6(config.go,connection_details.go) - Responsibility: Loads and parses
database.yml(with Go template expansion for env vars viagobuffalo/envy). CreatesConnectionDetailsstructs and registers them in the globalConnectionsmap. Supports multi-environment configs (development/test/production). - Key types:
ConnectionDetails,ErrConfigFileNotFound - Dependencies:
gobuffalo/envy,gopkg.in/yaml.v2,text/template
associations (sub-package)#
- Package:
github.com/gobuffalo/pop/v6/associations - Responsibility: Implements eager loading for BelongsTo, HasMany, HasOne, and ManyToMany associations via reflection on struct tags (
has_many,belongs_to,has_one,many_to_many). Called by the Query’s eager-loading path. - Key types:
Association(interface),AssociationSortable(interface), concrete association types - Dependencies: Root pop package (via circular reference at runtime via reflection, not import)
Data flow#
Typical read — c.Find(&user, id):
1. c.Find(&user, id)
→ Q(c).Find(&user, id) [finders.go]
→ q.Where("users.id = ?", id).First(&user)
→ q.ToSQL(model) [query.go]
→ sqlBuilder.compile() → "SELECT … FROM users WHERE users.id = ? LIMIT 1"
→ dialect.SelectOne(c, model, query) [dialect_postgresql.go etc.]
→ c.Store.Get(&user, sql, args...) [store: sqlx.DB.Get]
→ database/sql → network → PostgreSQLTypical write — c.Save(&user):
1. c.Save(&user) [executors.go]
→ c.ValidateAndSave or direct Save
→ model.beforeCreate(c) callback (if defined)
→ dialect.Create(c, model, columns) [dialect_postgresql.go]
→ sqlBuilder for INSERT
→ c.Store.NamedExec(sql, &user) [sqlx named params]
→ database/sql
→ model.afterCreate(c) callback (if defined)Migration run — soda db migrate:
1. cobra PersistentPreRun: pop.LoadConfigFile()
2. migrate cmd: conn := pop.Connections["development"]
3. conn.Open() [lazy connection]
4. migrator.Up()
→ scan migration files from directory
→ dialect.FizzTranslator().Translate(fizz) [fizz DSL → SQL]
→ conn.RawQuery(sql).Exec()
→ insert into schema_migrationInitialization / Bootstrap#
The library uses lazy, config-file-driven initialization with no dependency injection framework:
- Package-level
init()inconfig.goreadsAPP_PATH/POP_PATHenv vars to prime lookup paths. LoadConfigFile()is called explicitly (by user code orsoda’sPersistentPreRun). It: findsdatabase.yml, template-expands env vars, YAML-unmarshals intomap[string]*ConnectionDetails, callsNewConnection(deets)for each entry, and populates the globalConnectionsmap.NewConnection(deets)callsdeets.Finalize()(resolves URL/dialect defaults), then looks up a dialect factory in the privatenewConnectionmap (registry pattern — eachdialect_*.gofile callsinit()to register itself).Connect(env)orconn.Open()** opens the actual sqlx connection lazily (first time the connection is used).- No DI framework: Dependencies are wired by direct struct assignment (
c.Store = &dB{db},c.Dialect = pg) inside constructors.
Configuration#
- Primary mechanism: YAML file (
database.yml) discovered by walking a list oflookupPaths(.,./config,../, etc.) - Env var interpolation: The YAML file is first rendered as a Go
text/templateusinggobuffalo/envy. Template functionsenvOrandenvallow patterns like{{ envOr "DB_PASSWORD" "secret" }}. - CLI override:
soda --configflag sets a custom config path;--envselects the environment key (defaults to$GO_ENV, then"development"). - Connection pool config: Exposed directly in
ConnectionDetails(Pool,IdlePool,ConnMaxLifetime,ConnMaxIdleTime). - Instrumented driver:
UseInstrumentedDriver: trueinConnectionDetailswraps the driver withluna-duclos/instrumentedsqlfor distributed tracing.
Key design decisions#
dialectinterface as the portability seam. By composingcrudable,fizzable, andquotablesub-interfaces, Pop achieves multi-database support without if-chains in query execution code. Each dialect file is fully self-contained; adding a new database requires only a new file implementingdialectand a registration call ininit().storeinterface for context propagation without API breakage. Rather than adding actx context.Contextparameter to every public method (which would be a breaking change), Pop wraps the sqlx connection in acontextStorethat overrides every method to inject the context.Connection.WithContext(ctx)returns a copy with the wrapped store, enabling context propagation transparently.Registry pattern for dialect factories. The
newConnectionmap (map[string]func(*ConnectionDetails) (dialect, error)) lets each dialect file register itself viainit(). This avoids a central switch statement and allows build-tag-gated dialects (SQLite uses//go:build sqlite) without modifying core connection logic.Reflection-based model mapping (no code generation). Unlike GORM or sqlc, Pop derives table names, column names, and field mappings at runtime using reflection over struct tags (
db:"column_name"). This eliminates a build step at the cost of runtime overhead and deferred error detection.Flat root package as the primary API. All public types (
Connection,Query,Model,Migrator, etc.) live in the root package. This is a deliberate library-design choice: one import path gets everything. The trade-off — a large, dense package with 72.gofiles and broad internal coupling — is accepted in exchange for a simpler consumer experience.