Repository navigation
Realizing Capability Models for Data Access and Data Entities in Go
Authors:
- Takayuki Sato (GitHub: @sttk)
- Gemini (Large Language Model, Google)
This paper describes the design philosophy and implementation techniques of the Data Access Capability Model presented by "sabi," an application framework built in the Go programming language [1]. Furthermore, as an extended development of this model, this paper proposes an architectural approach that applies the Capability Model to data entities as data views in order to solve the inherent data modeling challenges faced by Domain-Driven Design (DDD) [2] and Object-Relational Mapping (ORM).
Traditional Dependency Injection (DI) and Repository patterns [2] tend to define interfaces based on the "possessions" (methods/fields) of modules or data. In contrast, sabi's data access model [1]—and the data entity model proposed in this paper—define the "minimal execution capabilities" and "minimal data views" required by business logic as types (interfaces). They then leverage Go's unique characteristics, namely structural typing (duck typing) and type embedding [3], to flexibly compose or carve out these capabilities.
This approach satisfies the Principle of Least Privilege [4] and the Interface Segregation Principle (ISP) [5] at a high level, enhancing system safety while maintaining excellent affinity with AI-driven code generation and comprehension.
In typical web application development, a repository interface is often defined in a one-to-one correspondence with a specific data source or table (e.g., User) [2].
type UserRepository interface {
Find(id int) (*User, error)
Save(user *User) error
Delete(id int) error
FindAll() ([]User, error)
}When injecting this interface into domain logic (e.g., UpdateUser), even if the logic only actually requires Find and Save, unnecessary access privileges like Delete and FindAll are still exposed within the logic. The framework sabi views this as a critical issue [1]: the interface boundaries are determined by the convenience of the data access layer (per database or table) rather than standing on the actual needs of the client (the business logic), which violates the Interface Segregation Principle [5].
A severe dilemma also exists in data model design within DDD [2] and ORM environments. Different use cases (such as registration, authentication, and profile display) require entirely different data fields. In response to this, if one defines separate, disjoint structures (such as DTOs [6]) for each use case, it becomes extremely difficult to propagate and reflect changes across all structures whenever the underlying data model changes (e.g., field additions or type modifications), severely degrading maintainability.
To avoid this maintenance cost, developers usually fall back on an "All-Powerful Domain Entity" that encompasses all persistence fields.
type User struct {
Id int64
Name string
Email string
Password string
Role string
}However, passing this all-powerful entity as-is into use cases that only require a subset of fields introduces a harsh trade-off between two major disadvantages:
-
Lack of Safety (Breeding Ground for Bugs)
If a repository partially fetches and populates only the fields needed for a specific use case (e.g.,
EmailandPasswordfor authentication), there is a constant risk that subsequent logic might accidentally access unpopulated fields (e.g.,NameorRole), leading to unexpected defects or runtime errors. - Sacrificing Performance (Wasted Costs) To mitigate this risk and guarantee safety, the data access layer must always fetch all fields from the database table and map them completely onto the entity. However, continuously fetching and mapping large amounts of unused data (e.g., hashed passwords, massive text fields, or joined tables) is highly inefficient in terms of network I/O and memory utilization, driving up overall system costs.
sabi addresses the first challenge (Section 1.1) by redefining data access concerns not as "possessions" but as "execution capabilities (Capabilities)" [1]. This concept was independently devised as a direct result of pursuing the strict separation between business logic and data access. Interestingly, this elegant engineering solution aligns perfectly with the established paradigms of Capability-Based Security [7] and Capability-Oriented Design.
In sabi, only the absolute minimum behaviors required by the logic are turned into an interface containing a single or a few methods [1].
type MyData interface {
GetText() (string, error)
SetText(string) error
}
func MyLogic(data MyData) error {
text, err := data.GetText()
if err != nil {
return err
}
return data.SetText(text)
}MyLogic depends solely on the MyData Capability. It remains completely oblivious to whether the underlying persistence layer is a relational database, an external HTTP API, or an in-memory cache.
The most distinctive implementation technique in sabi is the dynamic composition of Capabilities using Go's type embedding [1][3]. The concrete implementations of data access are broken down and defined at the granularity of individual Capabilities.
type GettingDataAcc struct {
sabi.DataAcc
}
func (d *GettingDataAcc) GetText() (string, error) {
return "sabi_data", nil
}
type SettingDataAcc struct {
sabi.DataAcc
}
func (d *SettingDataAcc) SetText(text string) error {
return nil
}These fragmented implementations are then embedded into an aggregation container called a DataHub [1].
type MyDataHub struct {
sabi.DataHub
*GettingDataAcc
*SettingDataAcc
}According to Go's language specifications, methods of embedded structs are automatically promoted to the outer struct (MyDataHub) [3]. Consequently, MyDataHub becomes a type equipped with both GetText() and SetText() without needing boilerplate code, naturally allowing the assignment var data MyData = myDataHub.
The DataHub serves as the core infrastructure that handles transaction boundaries, hides connection lifecycles, and acts as the unified interface aggregating the distributed Capabilities [1].
By directly extending sabi's core philosophy [1] from the data access layer to data structures, this section proposes a Data Entity Capability View Model designed to elegantly resolve the "all-powerful domain entity dilemma" described in Section 1.2.
In this proposal, the domain entity (User) is intentionally constructed as an "all-powerful implementation body" that comprehensively defines Getters and Setters for all fields. This ensures that any changes to the underlying database schema or core specifications can be absorbed and managed in one centralized location, eliminating the fragmentation drawbacks of scattered DTOs [6].
While centralizing Getters and Setters provides significant architectural safety, manually implementing these methods for entities with a massive number of fields introduces substantial boilerplate code. To prevent this from degrading developer experience, it is highly recommended to leverage Go's go generate directive alongside AST (Abstract Syntax Tree) parsing utilities. By automatically generating the comprehensive Getters, Setters, and even the Capability View interfaces from the struct definitions, teams can maintain the strict safety guarantees of this model while minimizing manual implementation costs.
type User struct {
Id int64
Name string
UserDetail *UserDetail // Detailed info carved out as a pointer
}
type UserDetail struct {
Email string
Password string
Role string
}
// Implement Getters/Setters for all fields to centralize change propagation
func (u *User) GetId() int64 { return u.Id }
func (u *User) GetName() string { return u.Name }
func (u *User) SetName(name string) { u.Name = name }
// Getters/Setters inside nested structures are implemented with nil-safety
func (u *User) GetEmail() string {
if u.UserDetail == nil {
return ""
}
return u.UserDetail.Email
}
func (u *User) GetPassword() string {
if u.UserDetail == nil {
return ""
}
return u.UserDetail.Password
}To bypass the downsides of the all-powerful entity—namely, accidental access to unpopulated fields and unnecessary data fetching costs—individual business logics are restricted from accepting the raw User struct directly. Instead, they demand an interface that defines only the data access privileges required for that specific use case (a Capability View).
Due to Go's structural typing (duck typing) [3], as long as the User struct satisfies all the Getters/Setters, it automatically fits any subset interface defined by the logic, requiring zero runtime object-mapping overhead.
// 1. Authentication use case: Exposes only specific read capabilities
type AuthUser interface {
GetEmail() string
GetPassword() string
}
func Authenticate(user AuthUser) error {
// Access to anything other than GetEmail() and GetPassword() is blocked at compile time.
// This completely prevents bugs caused by accidentally touching unpopulated fields.
return nil
}
// 2. Profile update use case: Exposes only specific read/write capabilities
type UpdatedUser interface {
GetName() string
SetName(string)
}
func UpdateProfile(user UpdatedUser, newName string) {
// The type system eliminates the possibility of accidentally overwriting passwords or roles.
user.SetName(newName)
}While the underlying instance remains the same User entity, developers can seamlessly switch its "role (window)" using duck typing when passing it to logic, such as var auth AuthUser = user.
User (Entity)
[All-powerful object with complete Getters/Setters]
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
AuthUser UpdatedUser ProfileUser
(GetEmail, (GetName, (GetName,
GetPassword) SetName) GetEmail)
[Auth View] [Update View] [Profile View]
When applying a highly constrained Capability View (e.g., UpdatedUser, which only updates the Name), allocating the entire User struct with all its fields (Email, Password, Role, etc.) in memory every single time still leaves room for optimization regarding computational resources.
To address this, this model proposes hierarchizing internal structs based on use-case cohesion and holding optional detailed data as pointers.
As shown in the code under Section 3.1, the UserDetail fields are separated into a nested struct pointer. This allows the data access layer (Repository)—in a context where only a name update (UpdatedUser) is required—to not only skip fetching UserDetail from the database but also **completely skip the memory allocation for the UserDetail instance itself, leaving it as nil**.
[Context of UpdatedUser (Name Update Only)]
User {
Id: 100,
Name: "Alice",
UserDetail: nil, <-- Memory allocation skipped (Zero waste)
}
Because the logic operates safely through the UpdatedUser interface boundary, it never needs to know that the internal structure has been optimized through lazy initialization. This achieves the best of both worlds: maintaining a single source of truth for schema changes while minimizing database fetching and memory allocation costs for specific use cases.
With this extension, a beautiful symmetry is established across the application architecture centered on sabi's design philosophy [1]:
-
Abstraction of Data Access (Behavior): Splitting the "data operation capabilities" required by logic into small Capability interfaces and synthesizing them via embedding in a
DataHub. (Current implementation of sabi [1]) - Abstraction of Data (State): Splitting the "data reference/mutation capabilities" owned by entities into small Capability View interfaces and slicing them out via structural typing. (Proposed extension in this paper)
This architecture represents the ultimate culmination of the Interface Segregation Principle (ISP) [5]. Business logic cannot perform any operations beyond the minimal interface it explicitly declares at compile time. It statically eliminates accidental field access while preserving the system's adaptability to changing data structures.
In an era where code generation and analysis by Large Language Models (LLMs) are becoming mainstream, this model offers a decisive edge over traditional approaches. Recent empirical data suggests that while AI code assistants dramatically increase output speed, they can lead to lower code quality and high duplication if the context is unclear [8].
With a traditional signature like func Authenticate(user *User), an AI must pull the entire bloated definition of User into its context and guess which fields the logic ought to utilize. Conversely, under sabi’s philosophy [1] and this proposal's format (func Authenticate(user AuthUser)), the AI can only see GetEmail() and GetPassword(). This eliminates guesswork and prevents hallucinations, enabling the AI to generate and reason about code with exceptional precision and speed, mitigating the risk of structural deterioration.
The data access Capability model in Go's sabi framework [1] shifts the role of interfaces from "defining what an object owns" to "declaring what capabilities the logic requires." The extension proposed in this paper directly applies this philosophy to data structures as Capability Views.
By comprehensively defining Getters and Setters on the entity side, data model modifications are managed centrally, while the internal structure is optimized via pointer-based composition. Concurrently, the client-side logic leverages Go's structural typing [3] to seamlessly substitute the entity with the ideal data view (AuthUser, UpdatedUser, etc.). This approach demonstrates that the trilemma of traditional domain modeling—namely "difficulty in propagating changes," "defects from accidental access," and "wasted data fetching/allocation costs"—can be elegantly resolved by maximizing the unique language features of Go.
-
[1] Sato, T. (@sttk). sabi: A Go package framework implementing the Data Access Capability Model. GitHub Repository.
https://github.com/sttk/sabi
- [2] Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley Professional. (Specifically regarding the Entity and Repository patterns).
- [3] The Go Authors. Effective Go: Embedding. Official Go Documentation (golang.org).
- [4] Saltzer, J. H., & Schroeder, M. D. (1975). The Protection of Information in Computer Systems. Proceedings of the IEEE, 63(9), 1278-1308. (Origin of the Principle of Least Privilege).
- [5] Martin, R. C. (2000). Design Principles and Design Patterns. Object Mentor. (Specifically regarding the Interface Segregation Principle).
- [6] Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley Professional. (Specifically regarding the Data Transfer Object pattern).
- [7] Mark Miller, K. Yee, J. Shapiro (2003). Capability Myths Demolished. Systems Research Laboratory (SRL) Technical Report SRL2003-02. (Theoretical alignment with Capability-Based Security).
- [8] William Harding(GitClear CEO / Lead Researcher)(2025). AI Copilot Code Quality: Evaluating 2024's Increased Defect Rate via Code Quality Metrics (Empirical evidence on AI code degradation without rigid interface boundaries).