Go Create Service
Generate service files for GO modular architechture conventions.
Three-File Pattern
Every service requires up to three files:
-
DTO structs (if needed): internal/modules/<module>/dto/<service_name>_dto.go
-
Port interface: internal/modules/<module>/ports/<service_name>_service.go
-
Service implementation: internal/modules/<module>/service/<service_name>_service.go
DTO File Layout Order
- Input/output structs
Port File Layout Order
- Interface definition (XxxService — no suffix)
Service File Layout Order
-
Implementation struct (XxxService )
-
Compile-time interface assertion
-
Constructor (NewXxxService )
-
Methods
DTO Structure
Location: internal/modules/<module>/dto/<service_name>_dto.go
package dto
type DoSomethingInput struct { Field string }
Port Interface Structure
Location: internal/modules/<module>/ports/<service_name>_service.go
package ports
import ( "context"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/dto"
)
type DoSomethingService interface { Execute(ctx context.Context, input dto.DoSomethingInput) error }
Service Implementation Structure
Location: internal/modules/<module>/service/<service_name>_service.go
package service
import ( "context"
"github.com/cristiano-pacheco/bricks/pkg/logger"
"github.com/cristiano-pacheco/bricks/pkg/otel/trace"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/dto"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/ports"
)
type DoSomethingService struct { logger logger.Logger // other dependencies }
var _ ports.DoSomethingService = (*DoSomethingService)(nil)
func NewDoSomethingService( logger logger.Logger, ) *DoSomethingService { return &DoSomethingService{ logger: logger, } }
func (s *DoSomethingService) Execute(ctx context.Context, input dto.DoSomethingInput) error { ctx, span := trace.Span(ctx, "DoSomethingService.Execute") defer span.End()
// Business logic here
// if err != nil {
// s.logger.Error("DoSomethingService.Execute failed", logger.Error(err))
// return err
// }
return nil
}
Service Variants
Single-action service (Execute pattern)
Use Execute method with a dedicated input struct when the service does one thing.
DTO (dto/send_email_confirmation_dto.go ):
type SendEmailConfirmationInput struct { UserModel model.UserModel ConfirmationTokenHash []byte }
Port (ports/send_email_confirmation_service.go ):
type SendEmailConfirmationService interface { Execute(ctx context.Context, input dto.SendEmailConfirmationInput) error }
Multi-method service (named methods)
Use descriptive method names when the service groups related operations.
Port (ports/hash_service.go ):
type HashService interface { GenerateFromPassword(password []byte) ([]byte, error) CompareHashAndPassword(hashedPassword, password []byte) error GenerateRandomBytes() ([]byte, error) }
Stateless service (no dependencies)
Omit logger and config when the service is a pure utility with no I/O.
type HashService struct{}
func NewHashService() *HashService { return &HashService{} }
Tracing
Services performing I/O MUST use trace.Span . Pure utilities (hashing, template compilation) skip tracing.
ctx, span := trace.Span(ctx, "ServiceName.MethodName") defer span.End()
Span name format: "StructName.MethodName"
Naming
- Port interface:
XxxService(inportspackage, no suffix) - Implementation struct:
XxxService(inservicepackage, same name — disambiguated by package) - Constructor:
NewXxxService, returns a pointer of the struct implementation
Fx Wiring
Add to internal/modules/<module>/fx.go:
fx.Provide(
fx.Annotate(
service.NewXxxService,
fx.As(new(ports.XxxService)),
),
),
Dependencies
Services depend on interfaces only. Common dependencies:
- logger.Logger
— structured logging
- Other ports.XxxService
interfaces — compose services
- ports.XxxRepository
— data access
- ports.XxxCache
— caching layer
Error Logging Rule
- Always use the Bricks logger package: github.com/cristiano-pacheco/bricks/pkg/logger
- Every time a service method returns an error, log it immediately before returning
- Preferred pattern:
if err != nil {
s.logger.Error("ServiceName.MethodName failed", logger.Error(err))
return err
}
Critical Rules
- Three files: DTOs in dto/
, port interface in ports/
, implementation in service/
- Interface in ports: Interface lives in ports/<name>_service.go
- DTOs in dto: Input/output structs live in dto/<name>_dto.go
- Interface assertion: Add var _ ports.XxxService = (*XxxService)(nil)
below the struct
- Constructor: MUST return pointer *XxxService
- Tracing: Every I/O method MUST use trace.Span
with defer span.End()
- Context: Methods performing I/O accept context.Context
as first parameter
- No comments on implementations: Do not add redundant comments above methods in the implementations
- Add detailed comment on interfaces: Provide comprehensive comments on the port interfaces to describe their purpose and usage
- Dependencies: Always depend on port interfaces, never concrete implementations
- Error logging: Every returned error must be logged first using Bricks logger (s.logger.Error(..., logger.Error(err))
)
Workflow
- Create DTO file in dto/<name>_dto.go
(if input/output structs are needed)
- Create port interface in ports/<name>_service.go
- Create service implementation in service/<name>_service.go
- Add Fx wiring to module's module.go
(or fx.go
)
- Run make lint
to verify
- Run make nilaway
for static analysis