CasaOS Service Layer Architecture: How the Repository Pattern Works and How to Extend It
CasaOS uses a repository-style service layer where a global Repository interface exposes feature-specific services through a centralized store struct, enabling loose coupling and easy extension via a six-step registration process.
The CasaOS service layer organizes all core business logic behind a clean abstraction that separates interface definitions from concrete implementations. This architecture, defined primarily in service/service.go, allows developers to add new capabilities by implementing small, focused interfaces and wiring them into the global MyService accessor. Understanding this pattern is essential for anyone contributing to the CasaOS codebase or building extensions that integrate with its system, storage, or notification features.
Global Service Accessor
CasaOS exposes all services through a single global variable declared in service/service.go.
var MyService Repository
This global acts as the single entry point for the entire application. Any package can retrieve a service using MyService.<Feature>(), such as MyService.System().GetDeviceInfo() (see service/system.go lines 72-78) or MyService.Notify().SendNotification(). The pattern eliminates circular dependencies while maintaining discoverability.
Repository Interface Design
The Repository interface defines the contract for accessing all domain services. Located in service/service.go, it exposes methods that return service-specific interfaces rather than concrete types.
type Repository interface {
Casa() CasaService
Connections() ConnectionsService
Gateway() external.ManagementService
Health() HealthService
Notify() NotifyServer
Rely() RelyService
Shares() SharesService
System() SystemService
Storage() StorageService
MessageBus() *message_bus.ClientWithResponses
Peer() PeerService
Other() OtherService
}
Each method returns an interface that abstracts the underlying implementation, allowing the concrete store to change without affecting consumers.
Concrete Store Implementation
The private store struct in service/service.go holds the actual service implementations. It aggregates dependencies like the database connection and individual service instances.
type store struct {
peer PeerService
db *gorm.DB
casa CasaService
notify NotifyServer
rely RelyService
system SystemService
shares SharesService
connections ConnectionsService
gateway external.ManagementService
storage StorageService
health HealthService
other OtherService
}
The NewService(db *gorm.DB, RuntimePath string) factory function initializes this struct (lines 48-66), injecting dependencies into each service constructor before returning the populated store as a Repository interface.
Service Implementation Patterns
Each feature lives in its own file and implements a minimal interface. CasaService (service/casa.go, lines 13-15) provides simple read-only version information. SystemService (service/system.go, lines 37-69) demonstrates DB-free logic with hardware queries and shell helpers. NotifyServer (service/notify.go, lines 25-39) shows a DB-backed service with WebSocket broadcasting capabilities.
Services access shared resources like the global cache (Cache) or configuration (config.*) without exposing these internals through their public interfaces.
Message Bus Integration
The MessageBus() method lazily initializes a client using CasaOS-Common utilities (service/service.go lines 29-46). Services publish events like casaos:file:operate (see service/notify.go lines 60-70) without direct coupling to the bus implementation. This allows asynchronous communication between features while maintaining the service layer's clean boundaries.
How to Extend the Service Layer
Adding new functionality to CasaOS follows a six-step registration process that maintains the architectural integrity of the repository pattern.
Step 1: Define the Interface
Create a new file like service/myfeature.go and define the public API.
type MyFeatureService interface {
Ping() string
DoSomething(arg string) error
}
Step 2: Implement the Service
Create a struct that implements the interface, accepting only required dependencies.
type myFeature struct {
db *gorm.DB
}
func (m *myFeature) Ping() string { return "pong" }
func (m *myFeature) DoSomething(arg string) error {
// Business logic here
return nil
}
func NewMyFeatureService(db *gorm.DB) MyFeatureService {
return &myFeature{db: db}
}
Step 3: Update the Repository Interface
Add a getter method to the Repository interface in service/service.go.
type Repository interface {
// ... existing methods ...
MyFeature() MyFeatureService
}
Step 4: Add Field to the Store
Add a field to the private store struct to hold the implementation.
type store struct {
// ... existing fields ...
myFeature MyFeatureService
}
Step 5: Initialize in NewService
Wire the constructor call inside the NewService factory function.
return &store{
// ... existing initializations ...
myFeature: NewMyFeatureService(db),
}
Step 6: Expose the Getter
Add the accessor method to the store struct.
func (s *store) MyFeature() MyFeatureService { return s.myFeature }
After these changes, any code can call MyService.MyFeature().Ping() to access the new functionality.
Testing Custom Services
Because services are accessed via interfaces, you can inject mocks for unit testing without modifying production code.
type mockFeature struct{}
func (m *mockFeature) Ping() string { return "mock-pong" }
func (m *mockFeature) DoSomething(arg string) error { return nil }
// In test setup:
MyService = &store{myFeature: &mockFeature{}}
This approach allows testing controllers and handlers in isolation from database or hardware dependencies.
Key Source Files
| File | Purpose |
|---|---|
service/service.go |
Defines Repository, MyService global, and NewService factory |
service/casa.go |
Simple read-only service example |
service/system.go |
Complex service with hardware and OS utilities |
service/notify.go |
DB-backed service with message bus integration |
service/storage.go |
Service delegating to external HTTP utilities |
Summary
- CasaOS uses a repository pattern where the
Repositoryinterface exposes domain services through a centralizedstorestruct. - Global access occurs via the
MyServicevariable, initialized once byNewServiceinservice/service.go. - Extension requires six steps: define interface, implement struct, add to
Repository, add tostore, initialize inNewService, and expose getter method. - Loose coupling allows services to depend only on the database, message bus, or configuration items they specifically need.
- Testability is built-in through interface-based design, enabling mock substitution for any service.
Frequently Asked Questions
What is the purpose of the MyService global variable in CasaOS?
The MyService global variable provides a centralized access point to all domain services defined in the Repository interface. Rather than passing service instances through function parameters or maintaining complex dependency injection containers, any package can call MyService.System() or MyService.Storage() to access the singleton service instances initialized at startup.
How does CasaOS handle database connections in the service layer?
The NewService factory accepts a *gorm.DB parameter and injects it into services that require persistence, such as NotifyServer and SharesService. Services that don't need database access, like CasaService or SystemService, receive only the parameters they require, maintaining clean separation between data access and business logic.
Can I add a service to CasaOS without modifying the core repository files?
No, extending CasaOS requires modifying service/service.go to add your service to the Repository interface, the store struct, and the NewService factory. However, you can isolate your implementation in a separate file (e.g., service/myfeature.go) and follow the existing pattern to minimize merge conflicts when updating from upstream.
How does the message bus fit into the service architecture?
The MessageBus() method returns a *message_bus.ClientWithResponses that services use to publish events without direct coupling to other services. Implemented in service/service.go lines 29-46, this client allows asynchronous communication—for example, the notify service publishes casaos:file:operate events that other services can subscribe to, maintaining loose coupling while enabling event-driven workflows.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →