| name | implementation-lokstra-create-service |
| description | Create @Service annotated infrastructure services. Generate repository implementations, data access objects, and service factory functions with dependency injection. Use after database schema is designed to implement data persistence layer. |
| phase | implementation |
| order | 4 |
| license | MIT |
| compatibility | {"lokstra_version":">=0.1.0","go_version":">=1.18"} |
Implementation: @Service Creation
When to Use
Use this skill when:
- Implementing repository layer (data persistence)
- Creating infrastructure services (database, cache, email, etc.)
- Setting up service factory with custom logic
- Configuring interface-based dependency injection
- Building swappable implementations for testing/multi-tenant
Prerequisites:
- ✅ Domain models and interfaces designed (see: design-lokstra-module-requirements)
- ✅ Database schema finalized (see: design-lokstra-schema-design)
- ✅ config.yaml with service definitions ready (see: implementation-lokstra-yaml-config)
Quick Reference
@Service Annotation
type MyService struct {
Dependency SomeInterface
}
Injection Patterns
| Pattern | Syntax | Description |
|---|
| Direct service | @Inject "service-name" | Inject registered service |
| Config service ref | @Inject "@config.key" | Service name from config |
| Config value | @Inject "cfg:key" | Direct config value |
| Indirect config | @Inject "cfg:@key" | Config value reference |
Service Types
1. Repository Service (Database Access)
The most common service type - implements data persistence using serviceapi.DbPool.
File: modules/user/infrastructure/postgres_user_repository.go
package infrastructure
import (
"context"
"fmt"
"myapp/modules/user/domain"
"myapp/modules/user/repository"
"github.com/primadi/lokstra/serviceapi"
)
type PostgresUserRepository struct {
dbPool serviceapi.DbPool
}
var _ repository.UserRepository = (*PostgresUserRepository)(nil)
func (r *PostgresUserRepository) Create(ctx context.Context, user *domain.User) error {
query := `
INSERT INTO users (id, name, email, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
`
_, err := r.dbPool.Exec(ctx, query,
user.ID, user.Name, user.Email, user.Status,
user.CreatedAt, user.UpdatedAt,
)
return err
}
func (r *PostgresUserRepository) Get(ctx context.Context, userID string) (*domain.User, error) {
query := `
SELECT id, name, email, status, created_at, updated_at
FROM users
WHERE id = $1
`
user := &domain.User{}
err := r.dbPool.QueryRow(ctx, query, userID).Scan(
&user.ID, &user.Name, &user.Email, &user.Status,
&user.CreatedAt, &user.UpdatedAt,
)
if r.dbPool.IsErrorNoRows(err) {
return nil, fmt.Errorf("user not found: %s", userID)
}
if err != nil {
return nil, err
}
return user, nil
}
func (r *PostgresUserRepository) Update(ctx context.Context, user *domain.User) error {
query := `
UPDATE users
SET name = $1, email = $2, status = $3, updated_at = $4
WHERE id = $5
`
result, err := r.dbPool.Exec(ctx, query,
user.Name, user.Email, user.Status, user.UpdatedAt, user.ID,
)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return fmt.Errorf("user not found: %s", user.ID)
}
return nil
}
func (r *PostgresUserRepository) Delete(ctx context.Context, userID string) error {
result, err := r.dbPool.Exec(ctx, "DELETE FROM users WHERE id = $1", userID)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return fmt.Errorf("user not found: %s", userID)
}
return nil
}
func (r *PostgresUserRepository) List(ctx context.Context) ([]*domain.User, error) {
query := `
SELECT id, name, email, status, created_at, updated_at
FROM users
ORDER BY created_at DESC
`
rows, err := r.dbPool.Query(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var users []*domain.User
for rows.Next() {
user := &domain.User{}
err := rows.Scan(
&user.ID, &user.Name, &user.Email, &user.Status,
&user.CreatedAt, &user.UpdatedAt,
)
if err != nil {
return nil, err
}
users = append(users, user)
}
return users, rows.Err()
}
func (r *PostgresUserRepository) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
query := `
SELECT id, name, email, status, created_at, updated_at
FROM users WHERE email = $1
`
user := &domain.User{}
err := r.dbPool.QueryRow(ctx, query, email).Scan(
&user.ID, &user.Name, &user.Email, &user.Status,
&user.CreatedAt, &user.UpdatedAt,
)
if r.dbPool.IsErrorNoRows(err) {
return nil, nil
}
return user, err
}
Interface Definition: modules/user/repository/user_repository.go
package repository
import (
"context"
"myapp/modules/user/domain"
)
type UserRepository interface {
Create(ctx context.Context, user *domain.User) error
Get(ctx context.Context, userID string) (*domain.User, error)
Update(ctx context.Context, user *domain.User) error
Delete(ctx context.Context, userID string) error
List(ctx context.Context) ([]*domain.User, error)
GetByEmail(ctx context.Context, email string) (*domain.User, error)
}
DbPool API Reference (serviceapi.DbPool)
Lokstra provides serviceapi.DbPool interface for database operations. This abstracts the underlying database driver (pgx for PostgreSQL).
Core Methods
type DbPool interface {
Acquire(ctx context.Context) (DbConn, error)
Exec(ctx context.Context, query string, args ...any) (CommandResult, error)
Query(ctx context.Context, query string, args ...any) (Rows, error)
QueryRow(ctx context.Context, query string, args ...any) Row
SelectOne(ctx context.Context, query string, args []any, dest ...any) error
SelectMustOne(ctx context.Context, query string, args []any, dest ...any) error
SelectOneRowMap(ctx context.Context, query string, args ...any) (RowMap, error)
SelectManyRowMap(ctx context.Context, query string, args ...any) ([]RowMap, error)
IsErrorNoRows(err error) bool
IsExists(ctx context.Context, query string, args ...any) (bool, error)
Transaction(ctx context.Context, fn func(tx DbExecutor) error) error
}
Usage Examples
func (r *UserRepo) GetByID(ctx context.Context, id string) (*User, error) {
user := &User{}
err := r.dbPool.QueryRow(ctx,
"SELECT id, name FROM users WHERE id = $1", id,
).Scan(&user.ID, &user.Name)
if r.dbPool.IsErrorNoRows(err) {
return nil, ErrUserNotFound
}
return user, err
}
func (r *UserRepo) GetByIDStrict(ctx context.Context, id string) (*User, error) {
user := &User{}
err := r.dbPool.SelectMustOne(ctx,
"SELECT id, name FROM users WHERE id = $1",
[]any{id}, &user.ID, &user.Name,
)
return user, err
}
func (r *UserRepo) GetUserMap(ctx context.Context, id string) (map[string]any, error) {
return r.dbPool.SelectOneRowMap(ctx,
"SELECT id, name, email FROM users WHERE id = $1", id,
)
}
func (r *UserRepo) EmailExists(ctx context.Context, email string) (bool, error) {
return r.dbPool.IsExists(ctx,
"SELECT 1 FROM users WHERE email = $1", email,
)
}
Transaction Support
Using Transaction Method
func (r *PostgresUserRepository) CreateWithAudit(ctx context.Context, user *domain.User) error {
return r.dbPool.Transaction(ctx, func(tx serviceapi.DbExecutor) error {
_, err := tx.Exec(ctx, `
INSERT INTO users (id, name, email) VALUES ($1, $2, $3)
`, user.ID, user.Name, user.Email)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `
INSERT INTO audit_logs (entity, action, entity_id, created_at)
VALUES ('user', 'create', $1, NOW())
`, user.ID)
if err != nil {
return err
}
return nil
})
}
Manual Transaction Control
func (r *PostgresUserRepository) TransferBalance(ctx context.Context, fromID, toID string, amount float64) error {
conn, err := r.dbPool.Acquire(ctx)
if err != nil {
return err
}
defer conn.Release()
tx, err := conn.Begin(ctx)
if err != nil {
return err
}
_, err = tx.Exec(ctx,
"UPDATE accounts SET balance = balance - $1 WHERE id = $2",
amount, fromID,
)
if err != nil {
tx.Rollback(ctx)
return err
}
_, err = tx.Exec(ctx,
"UPDATE accounts SET balance = balance + $1 WHERE id = $2",
amount, toID,
)
if err != nil {
tx.Rollback(ctx)
return err
}
return tx.Commit(ctx)
}
Multiple Implementations (Swappable)
Create different implementations of the same interface for different databases or environments.
PostgreSQL Implementation:
type PostgresUserRepository struct {
dbPool serviceapi.DbPool
}
var _ repository.UserRepository = (*PostgresUserRepository)(nil)
func (r *PostgresUserRepository) Get(ctx context.Context, id string) (*domain.User, error) {
}
In-Memory Implementation (for testing):
type InMemoryUserRepository struct {
mu sync.RWMutex
users map[string]*domain.User
}
var _ repository.UserRepository = (*InMemoryUserRepository)(nil)
func (r *InMemoryUserRepository) Init() error {
r.users = make(map[string]*domain.User)
return nil
}
func (r *InMemoryUserRepository) Get(ctx context.Context, id string) (*domain.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
user, exists := r.users[id]
if !exists {
return nil, fmt.Errorf("user not found: %s", id)
}
return user, nil
}
Select via config (no code change required):
configs:
repositories:
user-implementation: "postgres-user-repository"
service-definitions:
postgres-user-repository:
type: postgres-user-repository
depends-on: [db-main]
inmemory-user-repository:
type: inmemory-user-repository
Usage in handler:
type UserHandler struct {
UserRepo repository.UserRepository
}
Service Initialization
Services can implement an Init() method that is called after dependency injection.
type UserRepositoryImpl struct {
mu sync.RWMutex
users map[int]*domain.User
nextID int
byEmail map[string]*domain.User
}
func (r *UserRepositoryImpl) Init() error {
r.users = make(map[int]*domain.User)
r.byEmail = make(map[string]*domain.User)
r.nextID = 1
seedUsers := []*domain.User{
{Name: "Admin User", Email: "admin@example.com", Status: "active"},
{Name: "John Doe", Email: "john@example.com", Status: "active"},
}
for _, u := range seedUsers {
r.Create(u)
}
return nil
}
Built-in Infrastructure Services
Database Pool (PostgreSQL - dbpool_pg)
config.yaml:
service-definitions:
db-main:
type: dbpool_pg
config:
dsn: "${DATABASE_URL:postgres://postgres:admin@localhost:5432/mydb}"
schema: "public"
min-cons: 2
max-cons: 10
max-idle-time: "30m"
max-lifetime: "1h"
sslmode: "disable"
Register in main.go:
import "github.com/primadi/lokstra/services/dbpool_pg"
func main() {
lokstra.Bootstrap()
dbpool_pg.Register()
lokstra_init.BootstrapAndRun()
}
Config Parameters:
| Parameter | Type | Default | Description |
|---|
dsn | string | - | Full PostgreSQL connection string |
host | string | localhost | Database host (if no DSN) |
port | int | 5432 | Database port |
database | string | postgres | Database name |
username | string | postgres | Username |
password | string | - | Password |
schema | string | public | Default schema |
min-cons | int | 0 | Minimum pool connections |
max-cons | int | 4 | Maximum pool connections |
max-idle-time | duration | 30m | Max idle time before closing |
max-lifetime | duration | 1h | Max connection lifetime |
sslmode | string | disable | SSL mode |
rls-context | map | {} | Row-Level Security context |
Email Service (SMTP)
config.yaml:
service-definitions:
email-smtp:
type: email_smtp
config:
host: "${SMTP_HOST:smtp.gmail.com}"
port: ${SMTP_PORT:587}
username: "${SMTP_USER}"
password: "${SMTP_PASS}"
from_email: "noreply@myapp.com"
from_name: "MyApp"
use_starttls: true
Register in main.go:
import "github.com/primadi/lokstra/services/email_smtp"
func main() {
lokstra.Bootstrap()
email_smtp.Register()
lokstra_init.BootstrapAndRun()
}
Usage in service:
type NotificationService struct {
EmailSender serviceapi.EmailSender
}
func (s *NotificationService) SendWelcome(ctx context.Context, email, name string) error {
return s.EmailSender.Send(ctx, &serviceapi.EmailMessage{
To: []string{email},
Subject: "Welcome to MyApp!",
Body: fmt.Sprintf("Hello %s, welcome!", name),
})
}
Key-Value Store (PostgreSQL)
config.yaml:
service-definitions:
kvstore:
type: kvrepository_postgres
depends-on: [db-main]
config:
prefix: "app"
Register in main.go:
import "github.com/primadi/lokstra/services/kvstore/kvstore_postgres"
func main() {
lokstra.Bootstrap()
kvstore_postgres.Register()
lokstra_init.BootstrapAndRun()
}
Usage:
type CacheService struct {
KV serviceapi.KvRepository
}
func (s *CacheService) SetUserSession(ctx context.Context, userID string, data any) error {
return s.KV.Set(ctx, "session:"+userID, data, 24*time.Hour)
}
func (s *CacheService) GetUserSession(ctx context.Context, userID string, dest any) error {
return s.KV.Get(ctx, "session:"+userID, dest)
}
Key-Value Store (Redis)
config.yaml:
service-definitions:
kvstore-redis:
type: kvrepository_redis
config:
url: "${REDIS_URL:redis://localhost:6379}"
database: 0
prefix: "myapp"
Register in main.go:
import "github.com/primadi/lokstra/services/kvstore/kvstore_redis"
func main() {
lokstra.Bootstrap()
kvstore_redis.Register()
lokstra_init.BootstrapAndRun()
}
Lazy Loading Services
For optional dependencies or circular dependency resolution, use service.Cached:
import "github.com/primadi/lokstra/core/service"
type UserService struct {
dbPool serviceapi.DbPool
Logger *service.Cached[*LoggerService]
Cache *service.Cached[*CacheService]
}
func NewUserService() *UserService {
return &UserService{
Logger: service.LazyLoad[*LoggerService]("logger-service"),
Cache: service.LazyLoad[*CacheService]("cache-service"),
}
}
func (s *UserService) GetByID(ctx context.Context, id string) (*domain.User, error) {
if logger := s.Logger.Get(); logger != nil {
logger.Info("GetByID called", "id", id)
}
if cache := s.Cache.Get(); cache != nil {
if user, found := cache.GetUser(id); found {
return user, nil
}
}
}
Lazy Load Patterns
cache := service.LazyLoad[*CacheService]("cache-service")
db := service.LazyLoadWith(func() *DBPool {
return app.GetService("db-pool").(*DBPool)
})
mockCache := service.Value[*CacheService](mockCacheInstance)
user := s.Cache.MustGet().GetUser(id)
Dependency Injection Patterns
Direct Service Injection
type OrderService struct {
UserRepo repository.UserRepository
DB serviceapi.DbPool
}
Config Value Injection
type PaymentService struct {
Timeout time.Duration
APIKey string
SandboxMode bool
}
Service from Config (Interface Selection)
type NotificationService struct {
EmailProvider serviceapi.EmailSender
UserRepo repository.UserRepository
}
config.yaml:
configs:
providers:
email: "email-smtp"
repositories:
user: "postgres-user-repository"
service-definitions:
email-smtp:
type: email_smtp
config:
host: "${SMTP_HOST}"
email-sendgrid:
type: email_sendgrid
config:
api_key: "${SENDGRID_API_KEY}"
Error Handling Patterns
Using Custom Error Types
package domain
import "errors"
var (
ErrUserNotFound = errors.New("user not found")
ErrDuplicateEmail = errors.New("email already exists")
ErrInvalidInput = errors.New("invalid input")
)
func (r *PostgresUserRepository) Get(ctx context.Context, id string) (*domain.User, error) {
user := &domain.User{}
err := r.dbPool.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id).Scan(...)
if r.dbPool.IsErrorNoRows(err) {
return nil, domain.ErrUserNotFound
}
return user, err
}
func (h *UserHandler) GetByID(ctx *request.Context, id string) error {
user, err := h.UserRepo.Get(ctx.Request.Context(), id)
if errors.Is(err, domain.ErrUserNotFound) {
return ctx.Api.NotFound("user not found")
}
if err != nil {
return ctx.Api.InternalServerError("database error")
}
return ctx.Api.Ok(user)
}
Wrapping Errors with Context
func (r *PostgresUserRepository) Create(ctx context.Context, user *domain.User) error {
_, err := r.dbPool.Exec(ctx, `INSERT INTO users ...`, ...)
if err != nil {
if strings.Contains(err.Error(), "unique_violation") {
return fmt.Errorf("%w: %s", domain.ErrDuplicateEmail, user.Email)
}
return fmt.Errorf("failed to create user: %w", err)
}
return nil
}
File Organization
Recommended Structure
modules/user/
├── domain/
│ ├── user.go # Domain model
│ ├── user_dto.go # Request/Response DTOs
│ └── errors.go # Domain-specific errors
├── repository/
│ └── user_repository.go # Interface definition
├── infrastructure/
│ ├── postgres_user_repository.go # @Service PostgreSQL impl
│ ├── inmemory_user_repository.go # @Service in-memory impl (optional)
│ └── zz_generated.lokstra.go # Auto-generated
├── application/
│ ├── user_handler.go # @Handler
│ └── zz_generated.lokstra.go
Generated Code
The zz_generated.lokstra.go file is automatically generated and contains:
package infrastructure
import "github.com/primadi/lokstra/lokstra_registry"
func init() {
RegisterPostgresUserRepository()
}
func RegisterPostgresUserRepository() {
lokstra_registry.RegisterLazyService("postgres-user-repository",
func(deps map[string]any, cfg map[string]any) any {
svc := &PostgresUserRepository{
dbPool: deps["db-main"].(serviceapi.DbPool),
}
return svc
}, map[string]any{
"depends-on": []string{"db-main"},
})
}
Complete Example: Multi-Tenant Repository
package infrastructure
import (
"context"
"fmt"
"time"
"myapp/modules/tenant/domain"
"myapp/modules/tenant/repository"
"github.com/primadi/lokstra/common/json"
"github.com/primadi/lokstra/serviceapi"
)
type PostgresTenantRepository struct {
dbPool serviceapi.DbPool
}
var _ repository.TenantRepository = (*PostgresTenantRepository)(nil)
func (r *PostgresTenantRepository) Create(ctx context.Context, tenant *domain.Tenant) error {
metadata, _ := json.Marshal(tenant.Metadata)
settings, _ := json.Marshal(tenant.Settings)
query := `
INSERT INTO tenants (
id, name, domain, db_dsn, db_schema, status,
settings, metadata, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
`
_, err := r.dbPool.Exec(ctx, query,
tenant.ID, tenant.Name, tenant.Domain, tenant.DBDsn, tenant.DBSchema,
tenant.Status, settings, metadata, time.Now(), time.Now(),
)
return err
}
func (r *PostgresTenantRepository) Get(ctx context.Context, tenantID string) (*domain.Tenant, error) {
query := `
SELECT id, name, domain, db_dsn, db_schema, status,
settings, metadata, created_at, updated_at
FROM tenants WHERE id = $1
`
tenant := &domain.Tenant{}
var metadata, settings []byte
err := r.dbPool.QueryRow(ctx, query, tenantID).Scan(
&tenant.ID, &tenant.Name, &tenant.Domain, &tenant.DBDsn, &tenant.DBSchema,
&tenant.Status, &settings, &metadata, &tenant.CreatedAt, &tenant.UpdatedAt,
)
if r.dbPool.IsErrorNoRows(err) {
return nil, fmt.Errorf("tenant not found: %s", tenantID)
}
if err != nil {
return nil, err
}
if len(metadata) > 0 {
json.Unmarshal(metadata, &tenant.Metadata)
}
if len(settings) > 0 {
json.Unmarshal(settings, &tenant.Settings)
}
return tenant, nil
}
func (r *PostgresTenantRepository) List(ctx context.Context) ([]*domain.Tenant, error) {
query := `
SELECT id, name, domain, status, created_at, updated_at
FROM tenants
WHERE deleted_at IS NULL
ORDER BY created_at DESC
`
rows, err := r.dbPool.Query(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var tenants []*domain.Tenant
for rows.Next() {
t := &domain.Tenant{}
err := rows.Scan(
&t.ID, &t.Name, &t.Domain, &t.Status,
&t.CreatedAt, &t.UpdatedAt,
)
if err != nil {
return nil, err
}
tenants = append(tenants, t)
}
return tenants, rows.Err()
}
func (r *PostgresTenantRepository) SoftDelete(ctx context.Context, tenantID string) error {
query := `UPDATE tenants SET deleted_at = NOW() WHERE id = $1`
result, err := r.dbPool.Exec(ctx, query, tenantID)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return fmt.Errorf("tenant not found: %s", tenantID)
}
return nil
}
Next Steps
- Create database migrations (see: implementation-lokstra-create-migrations)
- Create @Handler endpoints (see: implementation-lokstra-create-handler)
- Configure service definitions (see: implementation-lokstra-yaml-config)
- Write integration tests (see: advanced-lokstra-tests)
Related Skills