A practical reference for engineers moving from Spring Boot (Kotlin/Java) to Go (go-zero + GORM). Based on the
company-crm-servicecodebase.
- Philosophy: What Changes, What Doesn't
- Project Structure
- Request Lifecycle
- Entities & Models
- Repository → Model Layer
- Service → Logic Layer
- Controller → Handler Layer
- Route Registration
- Dependency Injection
- Configuration
- Error Handling
- Middleware / Filters
- HTTP Client (Feign → Go)
- Database Migrations
- Go Concepts That Have No Spring Equivalent
- Quick Reference Cheat Sheet
- 3-layer architecture — Controller → Service → Repository maps to Handler → Logic → Model
- ORM — GORM works like Hibernate/Spring Data JPA
- YAML config — same idea, different structure
- Migrations — Liquibase is used in both
| Spring Boot | Go |
|---|---|
| Framework does the wiring (DI, annotations, scanning) | You do the wiring — manually, explicitly |
Annotations everywhere (@Service, @Autowired) |
No annotations — plain structs and interfaces |
| Exceptions for error flow | Errors are return values — (result, error) |
Implicit null via Optional<T> |
Explicit null via pointers *string, *int |
| Spring manages threads | You manage goroutines |
| Maven/Gradle | go.mod (much simpler) |
| JVM startup overhead | Compiles to a single binary, starts in milliseconds |
In Spring, the framework finds your code and runs it. In Go, your code is always explicit — you can trace every call by reading top to bottom.
There is no magic. No proxies. No AOP that silently wraps your methods. What you write is what runs.
Spring Boot (Kotlin) Go (go-zero + GORM)
───────────────────────────────────── ──────────────────────────────────────
src/main/java/com/company/
├── controller/ internal/handler/
│ └── EmployeeController.kt ├── employee/
│ │ ├── createemployeehandler.go
│ │ └── getemployeehandler.go
│ ├── zone/
│ └── routes.go ← ALL routes here
│
├── service/ internal/logic/
│ ├── EmployeeService.kt (interface) ├── employee/
│ └── EmployeeServiceImpl.kt ├── createemployeelogic.go
│ └── getemployeelogic.go
│ └── zone/
│
├── repository/ model/
│ └── EmployeeRepository.kt ├── employeemodel.go ← interface + GORM impl
│ └── zonemodel.go
│
├── entity/ model/ ← same file as repository
│ └── Employee.kt (@Entity)
│
├── dto/ internal/types/
│ ├── CreateEmployeeRequest.kt └── types.go
│ └── EmployeeResponse.kt
│
├── config/ internal/config/
│ └── AppConfig.kt ├── config.go
│ └── database.go
│
├── exception/ model/vars.go
│ └── GlobalExceptionHandler.kt + httpx.ErrorCtx() in handlers
│
├── interceptor/ / @Aspect internal/middleware/
│ └── AuthInterceptor.kt └── scopemiddleware.go
│
└── Application.kt sample.go ← main()
src/main/resources/
├── application.yml etc/company-crm-api-local.yaml
└── db/migration/ (Flyway) database/migrations/ (Liquibase)
pom.xml / build.gradle go.mod
HTTP Request
→ DispatcherServlet
→ Filter chain (@Aspect, interceptors)
→ @RestController method
→ @Service method
→ @Repository method (Spring Data JPA)
→ Hibernate → DB
HTTP Request
→ go-zero router (registered in routes.go)
→ Middleware chain (scopemiddleware.go)
→ HandlerFunc (e.g. createemployeehandler.go)
→ Logic (e.g. createemployeelogic.go)
→ Model method (e.g. employeemodel.go)
→ GORM → DB
They are structurally identical. The difference is Spring automates the wiring; Go makes you do it once in routes.go and servicecontext.go.
@Entity
@Table(name = "employees")
data class Employee(
@Id @GeneratedValue(strategy = GenerationType.UUID)
val id: UUID = UUID.randomUUID(),
@Column(name = "user_id", nullable = false, unique = true)
val userId: UUID,
@Column(nullable = false)
val designation: String,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "manager_id")
val manager: Employee? = null,
@OneToMany(mappedBy = "manager")
val reports: List<Employee> = emptyList(),
@ManyToMany
@JoinTable(
name = "employee_zone_assignments",
joinColumns = [JoinColumn(name = "employee_id")],
inverseJoinColumns = [JoinColumn(name = "zone_id")]
)
val zones: List<Zone> = emptyList(),
@Column(name = "is_active")
val isActive: Boolean = true,
@CreatedDate val createdAt: LocalDateTime = LocalDateTime.now(),
@LastModifiedDate val updatedAt: LocalDateTime = LocalDateTime.now()
)// model/employeemodel.go
type Employee struct {
ID string `gorm:"type:uuid;primaryKey"`
UserID string `gorm:"type:uuid;uniqueIndex;not null"`
Designation string `gorm:"not null"`
ManagerID *string `gorm:"type:uuid;index"`
Manager *Employee `gorm:"foreignKey:ManagerID"` // @ManyToOne
Reports []Employee `gorm:"foreignKey:ManagerID"` // @OneToMany
Zones []Zone `gorm:"many2many:employee_zone_assignments;..."` // @ManyToMany
IsActive bool `gorm:"not null;default:true"`
CreatedAt time.Time
UpdatedAt time.Time
}
// UUID auto-generated before insert (replaces @GeneratedValue)
func (e *Employee) BeforeCreate(_ *gorm.DB) error {
if e.ID == "" {
e.ID = uuid.New().String()
}
return nil
}| Spring / JPA | GORM struct tag |
|---|---|
@Id |
gorm:"primaryKey" |
@GeneratedValue |
BeforeCreate hook |
@Column(nullable = false) |
gorm:"not null" |
@Column(unique = true) |
gorm:"uniqueIndex" |
@Column(name = "user_id") |
gorm:"column:user_id" (or snake_case auto-mapping) |
@ManyToOne |
gorm:"foreignKey:FieldName" |
@OneToMany(mappedBy) |
gorm:"foreignKey:FieldName" |
@ManyToMany + @JoinTable |
gorm:"many2many:table_name" |
@CreatedDate |
CreatedAt time.Time (GORM auto-fills) |
@LastModifiedDate |
UpdatedAt time.Time (GORM auto-fills) |
// Spring — lazy loading is automatic (can cause N+1)
val manager = employee.manager // triggered lazily// Go — GORM requires explicit Preload (no lazy loading)
db.Preload("Manager").First(&employee, "id = ?", id)
db.Preload("Zones").Preload("Manager").First(&employee, "id = ?", id)
// This is actually better — you always know what queries run@Repository
interface EmployeeRepository : JpaRepository<Employee, UUID> {
// Spring Data auto-generates from method names:
fun findByUserId(userId: UUID): Optional<Employee>
fun findAllByDesignation(designation: String): List<Employee>
fun findAllByManagerId(managerId: UUID): List<Employee>
}// model/employeemodel.go
// Step 1: Define the interface (like the Repository interface in Spring)
type EmployeeModel interface {
Insert(ctx context.Context, emp *Employee) error
FindByID(ctx context.Context, id string) (*Employee, error)
FindByUserID(ctx context.Context, userID string) (*Employee, error)
List(ctx context.Context, filter EmployeeFilter) ([]*Employee, error)
Update(ctx context.Context, emp *Employee) error
}
// Step 2: Implement it (Spring Data auto-generates this; you write it in Go)
type gormEmployeeModel struct{ db *gorm.DB }
func NewEmployeeModel(db *gorm.DB) EmployeeModel {
return &gormEmployeeModel{db: db}
}
func (m *gormEmployeeModel) FindByID(ctx context.Context, id string) (*Employee, error) {
var emp Employee
err := m.db.WithContext(ctx).First(&emp, "id = ?", id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return &emp, err
}
func (m *gormEmployeeModel) List(ctx context.Context, filter EmployeeFilter) ([]*Employee, error) {
q := m.db.WithContext(ctx).Model(&Employee{})
if filter.Designation != "" {
q = q.Where("designation = ?", filter.Designation)
}
if filter.ManagerID != "" {
q = q.Where("manager_id = ?", filter.ManagerID)
}
var employees []*Employee
return employees, q.Order("created_at ASC").Find(&employees).Error
}Key difference: Spring Data generates queries from method names magically. In Go you write the queries yourself — more code, nothing hidden, easier to debug.
@Service
class CreateEmployeeService(
private val employeeRepository: EmployeeRepository, // injected automatically
private val logger: Logger = LoggerFactory.getLogger(CreateEmployeeService::class.java)
) {
fun createEmployee(request: CreateEmployeeRequest): EmployeeResponse {
if (employeeRepository.existsByUserId(request.userId)) {
throw ConflictException("Employee already exists")
}
val emp = Employee(userId = request.userId, designation = request.designation)
return employeeRepository.save(emp).toResponse()
}
}// internal/logic/employee/createemployeelogic.go
type CreateEmployeeLogic struct {
logx.Logger // embedded logger (like Slf4j — auto-available as l.Info(), l.Error())
ctx context.Context
svcCtx *svc.ServiceContext // holds all dependencies (replaces @Autowired fields)
}
func NewCreateEmployeeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateEmployeeLogic {
return &CreateEmployeeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CreateEmployeeLogic) CreateEmployee(req *types.CreateEmployeeRequest) (*types.EmployeeResponse, error) {
designation, err := domain.ParseDesignation(req.Designation)
if err != nil {
return nil, err // no exceptions — return error as value
}
emp := &model.Employee{
UserID: req.UserID,
Designation: designation.String(),
ManagerID: req.ManagerID,
IsActive: true,
}
if err := l.svcCtx.EmployeeModel.Insert(l.ctx, emp); err != nil {
return nil, err
}
return toEmployeeResponse(emp), nil
}Unlike Spring where one @Service class handles all employee operations, in go-zero each use case gets its own file:
internal/logic/employee/
├── createemployeelogic.go ← CreateEmployeeService
├── getemployeelogic.go ← GetEmployeeService / findById
├── listemployeeslogic.go ← ListEmployeeService / findAll
├── updateemployeelogic.go ← UpdateEmployeeService
├── assignzonelogic.go ← AssignZoneService
└── helpers.go ← shared toEmployeeResponse() converter
@RestController
@RequestMapping("/v1/employees")
class EmployeeController(
private val createEmployeeService: CreateEmployeeService
) {
@PostMapping
@ResponseStatus(HttpStatus.OK)
fun create(@RequestBody @Valid request: CreateEmployeeRequest): EmployeeResponse {
return createEmployeeService.createEmployee(request)
}
@GetMapping("/{id}")
fun getById(@PathVariable id: UUID): EmployeeResponse {
return getEmployeeService.getEmployee(id)
}
}// internal/handler/employee/createemployeehandler.go
func CreateEmployeeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.CreateEmployeeRequest
if err := httpx.Parse(r, &req); err != nil { // like @RequestBody + @Valid
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := employeelogic.NewCreateEmployeeLogic(r.Context(), svcCtx)
resp, err := l.CreateEmployee(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err) // like @ExceptionHandler
return
}
httpx.OkJsonCtx(r.Context(), w, resp)
}
}| Spring annotation | Go struct tag | What it does |
|---|---|---|
@PathVariable id |
ID string \path:"id"`` |
Read from URL path |
@RequestParam name |
Name string \form:"name"`` |
Read from query string |
@RequestBody |
json:"field_name" |
Read from JSON body |
@RequestParam(required=false) |
form:"name,optional" |
Optional query param |
All resolved by a single call: httpx.Parse(r, &req)
@RestController
@RequestMapping("/v1/employees") // ← route here
class EmployeeController {
@PostMapping // ← route here
fun create(...) {}
@GetMapping("/{id}") // ← route here
fun getById(...) {}
}
// Spring finds controllers via @ComponentScan automatically// internal/handler/routes.go ← single source of truth for all routes
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.ScopeMiddleware},
[]rest.Route{
// Zones
{Method: http.MethodPost, Path: "/v1/zones", Handler: zonehandler.CreateZoneHandler(serverCtx)},
{Method: http.MethodGet, Path: "/v1/zones", Handler: zonehandler.ListZonesHandler(serverCtx)},
{Method: http.MethodGet, Path: "/v1/zones/:id", Handler: zonehandler.GetZoneHandler(serverCtx)},
{Method: http.MethodPatch, Path: "/v1/zones/:id", Handler: zonehandler.UpdateZoneHandler(serverCtx)},
// Employees
{Method: http.MethodPost, Path: "/v1/employees", Handler: employeehandler.CreateEmployeeHandler(serverCtx)},
{Method: http.MethodGet, Path: "/v1/employees", Handler: employeehandler.ListEmployeesHandler(serverCtx)},
{Method: http.MethodGet, Path: "/v1/employees/:id", Handler: employeehandler.GetEmployeeHandler(serverCtx)},
{Method: http.MethodPatch, Path: "/v1/employees/:id", Handler: employeehandler.UpdateEmployeeHandler(serverCtx)},
{Method: http.MethodPost, Path: "/v1/employees/:id/zones", Handler: employeehandler.AssignZoneHandler(serverCtx)},
{Method: http.MethodGet, Path: "/v1/employees/:id/zones", Handler: employeehandler.ListEmployeeZonesHandler(serverCtx)},
{Method: http.MethodDelete, Path: "/v1/employees/:id/zones/:zone_id", Handler: employeehandler.RemoveZoneHandler(serverCtx)},
}...,
),
)
}Advantage: You can see every single endpoint the service exposes in one file. No hunting through 20 controllers.
@SpringBootApplication
fun main(args: Array<String>) {
runApplication<Application>(*args)
// Spring scans all @Component, @Service, @Repository, @Controller
// Wires everything automatically via @Autowired
}
@Service
class CreateEmployeeService(
private val employeeRepository: EmployeeRepository, // auto-injected
private val zoneRepository: ZoneRepository // auto-injected
)// internal/svc/servicecontext.go ← the "ApplicationContext" of this service
type ServiceContext struct {
Config config.Config
DB *gorm.DB
ScopeMiddleware rest.Middleware
EmployeeModel model.EmployeeModel // ← like @Autowired EmployeeRepository
ZoneModel model.ZoneModel
EZAModel model.EmployeeZoneAssignmentModel
}
func NewServiceContext(c config.Config, db *gorm.DB) *ServiceContext {
return &ServiceContext{
Config: c,
DB: db,
EmployeeModel: model.NewEmployeeModel(db), // ← you wire it yourself
ZoneModel: model.NewZoneModel(db),
EZAModel: model.NewEmployeeZoneAssignmentModel(db),
}
}
// sample.go — entry point
func main() {
db := config.InitializeDatabase(c) // like @Bean DataSource
ctx := svc.NewServiceContext(c, db) // like ApplicationContext
handler.RegisterHandlers(server, ctx)
server.Start()
}Go has no DI framework.
ServiceContextis passed to every Logic and every Handler. Adding a new dependency? Add it toServiceContextand pass it through. Simple.
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/company_crm
username: postgres
password: secret
jpa:
show-sql: true
server:
port: 8080
custom:
identity-service-url: http://identity-service@ConfigurationProperties(prefix = "custom")
data class AppConfig(val identityServiceUrl: String)# etc/company-crm-api-local.yaml
Name: company-crm-api
Host: 0.0.0.0
Port: 5000
Environment: dev
DatabaseName: company_crm
Company:
Datasource:
EmployeeDb:
Url: localhost:5432
Username: postgres
Password: secret// internal/config/config.go
type Config struct {
rest.RestConf // Name, Host, Port embedded
Environment string
DatabaseName string
Company struct {
Datasource struct {
EmployeeDb struct {
URL string
Username string
Password string
}
}
} `json:",optional"`
}
// Loading (replaces @ConfigurationProperties)
conf.MustLoad(*configFile, &c) // crashes fast if config is missing — intentionalThis is the biggest conceptual shift from Spring to Go.
// Throw anywhere, catch at global handler
@Service
class GetEmployeeService(private val repo: EmployeeRepository) {
fun getEmployee(id: UUID): EmployeeResponse {
return repo.findById(id)
.orElseThrow { NotFoundException("Employee $id not found") }
.toResponse()
}
}
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException::class)
@ResponseStatus(HttpStatus.NOT_FOUND)
fun handleNotFound(ex: NotFoundException) = ErrorResponse(ex.message)
}// Errors are returned, not thrown
func (l *GetEmployeeLogic) GetEmployee(req *types.GetEmployeeRequest) (*types.EmployeeResponse, error) {
emp, err := l.svcCtx.EmployeeModel.FindByID(l.ctx, req.ID)
if err != nil {
return nil, err // caller handles it
}
return toEmployeeResponse(emp), nil
}
// Handler handles the error — like @ExceptionHandler but inline
func GetEmployeeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
resp, err := l.GetEmployee(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err) // writes error JSON response
return
}
httpx.OkJsonCtx(r.Context(), w, resp)
}
}// Rule 1: Always check errors — ignoring is a bug
result, err := someOperation()
if err != nil {
return nil, err
}
// Rule 2: Wrap errors with context
return nil, fmt.Errorf("finding employee %s: %w", id, err)
// Rule 3: Sentinel errors for known cases
var ErrNotFound = gorm.ErrRecordNotFound
if errors.Is(err, model.ErrNotFound) {
// handle not found specifically
}@Component
class AuthFilter : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
chain: FilterChain
) {
val token = request.getHeader("Authorization")
if (!isValid(token)) {
response.status = 401
return
}
chain.doFilter(request, response)
}
}
// Or AOP:
@Aspect @Component
class LoggingAspect {
@Around("@annotation(Logged)")
fun logExecution(jp: ProceedingJoinPoint): Any { ... }
}// internal/middleware/scopemiddleware.go
type ScopeMiddleware struct{ env string }
func (m *ScopeMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if m.env == "prod" {
token := r.Header.Get("Authorization")
if !isValid(token) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
next(w, r) // like chain.doFilter()
}
}
// Applied in routes.go:
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.ScopeMiddleware},
routes...,
)@FeignClient(name = "identity-service", url = "\${identity.service.url}")
interface IdentityClient {
@GetMapping("/v1/users/{id}")
fun getUser(@PathVariable id: UUID): UserResponse
@PostMapping("/v1/users")
fun createUser(@RequestBody request: CreateUserRequest): UserResponse
}
// Usage — Spring injects automatically
@Service
class EmployeeService(private val identityClient: IdentityClient)// No Feign equivalent built-in — write a thin client wrapper
type IdentityClient struct {
baseURL string
httpClient *http.Client
}
func NewIdentityClient(baseURL string) *IdentityClient {
return &IdentityClient{
baseURL: baseURL,
httpClient: &http.Client{Timeout: 10 * time.Second},
}
}
func (c *IdentityClient) GetUser(ctx context.Context, id string) (*UserResponse, error) {
url := fmt.Sprintf("%s/v1/users/%s", c.baseURL, id)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("building request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("calling identity service: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, ErrNotFound
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("identity service returned %d", resp.StatusCode)
}
var user UserResponse
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
return &user, nil
}
// Add to ServiceContext like any other dependency
type ServiceContext struct {
IdentityClient *client.IdentityClient
// ...
}Both Spring and this project use Liquibase. Syntax is identical.
-- src/main/resources/db/migration/V1__create_employees.sql
CREATE TABLE employees (
id UUID PRIMARY KEY,
user_id UUID NOT NULL UNIQUE,
designation VARCHAR(50) NOT NULL
);-- database/migrations/common/002-create-employees.sql
--liquibase formatted sql
--changeset company:002-create-employees
CREATE TABLE IF NOT EXISTS employees (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
designation TEXT NOT NULL,
manager_id UUID REFERENCES employees(id) ON DELETE RESTRICT,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_employees_user_id UNIQUE (user_id)
);Run migrations:
docker compose up liquibase # equivalent to running Flyway in SpringNote: We do NOT use GORM's
AutoMigrate. Always use explicit SQL migrations — easier to review, version, and roll back.
Every operation that touches the network or DB takes a ctx. It carries:
- Cancellation — if the HTTP request times out, ctx is cancelled, DB queries abort
- Tracing — request IDs and trace spans propagate automatically
- Deadlines — you can set "this operation must finish in 5 seconds"
// Always pass ctx through — don't ignore it
func (m *gormEmployeeModel) FindByID(ctx context.Context, id string) (*Employee, error) {
return ..., m.db.WithContext(ctx).First(&emp, "id = ?", id).Error
// ^^^^^^^^^^^^^ cancellation propagates to DB query
}Spring handles this with @Async, @Transactional propagation, and Reactor — all implicit. In Go it's always explicit.
Like threads but managed by Go's runtime, extremely lightweight (2KB stack vs 1MB for JVM threads).
// Run something in the background
go func() {
// this runs concurrently
sendNotification(employeeID)
}()
// Spring equivalent: @Async methodGo has no Optional<T> or null objects. Use pointers:
type Employee struct {
ManagerID *string // nil = no manager (like Optional.empty())
// &"some-uuid" = has manager (like Optional.of())
}
// Check nil before using
if emp.ManagerID != nil {
manager, err := l.svcCtx.EmployeeModel.FindByID(ctx, *emp.ManagerID)
}// Define interface
type EmployeeModel interface {
FindByID(ctx context.Context, id string) (*Employee, error)
}
// Implement it — no "implements" keyword needed
type gormEmployeeModel struct{ db *gorm.DB }
func (m *gormEmployeeModel) FindByID(ctx context.Context, id string) (*Employee, error) {
// ...
}
// gormEmployeeModel automatically satisfies EmployeeModel
// The compiler checks this — if a method is missing, it's a compile errorIn Spring you write class EmployeeServiceImpl : EmployeeService. In Go, if a type has the right methods, it already satisfies the interface — no declaration needed.
| Spring Boot | Go (this project) |
|---|---|
@RestController |
handler/ package |
@Service |
logic/ package |
@Repository |
model/ package (interface) |
@Entity |
GORM struct |
@Autowired |
ServiceContext field |
ApplicationContext |
ServiceContext |
@SpringBootApplication + main() |
sample.go main() |
application.yml |
etc/*.yaml |
@ExceptionHandler |
httpx.ErrorCtx() in handler |
@Transactional |
db.Transaction(func(tx){...}) |
Optional<T> |
*T (pointer, check for nil) |
throws Exception |
return nil, err |
@Aspect / Filter |
Middleware function |
| Feign Client | HTTP client struct |
pom.xml |
go.mod |
| Flyway/Liquibase | Liquibase (same) |
// findById
db.First(&emp, "id = ?", id)
// findAll
db.Find(&employees)
// findAll with condition
db.Where("designation = ?", "TL").Find(&employees)
// save (insert or update)
db.Create(&emp) // INSERT
db.Save(&emp) // UPDATE (full record)
db.Updates(&emp) // UPDATE (only non-zero fields)
// delete
db.Delete(&emp)
// with associations (like .fetch = EAGER)
db.Preload("Manager").Preload("Zones").First(&emp, "id = ?", id)
// transaction (like @Transactional)
db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&emp).Error; err != nil {
return err // auto-rollback
}
return tx.Create(&eza).Error
})# Start dependencies
docker compose up postgres
# Run migrations
make migrate
# Start the service (dev mode)
make dev
# or: go run sample.go -f etc/company-crm-api-local.yaml
# Build binary (like mvn package)
go build -o company-crm-service .Last updated: April 2026 — company-crm-service v1