Layered Architecture #

Di antara semua architectural pattern yang ada, Layered Architecture adalah yang paling banyak diimplementasikan, paling sering diajarkan, dan paling mudah dipahami oleh developer baru. Jika kamu pernah melihat codebase dengan folder handler, service, repository, dan model — itulah Layered Architecture dalam salah satu variasinya. Kesederhanaannya adalah kekuatan sekaligus kelemahannya: mudah untuk mulai, tapi juga mudah untuk salah dalam cara yang tidak langsung terlihat. Business logic yang perlahan merayap ke handler, entity yang tightly coupled ke database schema, service layer yang menjadi tempat semua hal dilempar — semua ini adalah jebakan umum yang sering baru terasa menyakitkan bertahun-tahun kemudian. Artikel ini membahas Layered Architecture bukan hanya sebagai “cara menyusun folder”, tapi sebagai seperangkat aturan yang, jika ditegakkan dengan disiplin, menghasilkan codebase yang mudah dipahami, diuji, dan dikembangkan dalam jangka panjang.

Struktur Empat Layer #

Layered Architecture memisahkan sistem ke dalam lapisan horizontal dengan aturan dependency yang jelas: lapisan atas bergantung ke lapisan bawah, tidak pernah sebaliknya.

flowchart TD
    P["Presentation Layer\nHTTP Handler · gRPC Server · CLI"]
    A["Application Layer\nService · Use Case · Orchestration"]
    D["Domain Layer\nEntity · Business Rules · Repository Interface"]
    I["Infrastructure Layer\nDatabase · Cache · Message Broker · External API"]

    P -->|"depends on"| A
    A -->|"depends on"| D
    D -.->|"interface implemented by"| I

    style P fill:#74c69d,color:#1b4332
    style A fill:#52b788,color:#fff
    style D fill:#2d6a4f,color:#fff
    style I fill:#b7e4c7,color:#1b4332

Setiap layer punya satu tanggung jawab yang jelas dan tidak tumpang tindih:

Layer Tanggung Jawab Tidak Boleh
Presentation Parse request, validasi format, format response Business logic, akses database langsung
Application Orkestrasi use case, transaksi, koordinasi antar service Detail HTTP, format JSON/XML
Domain Business rules, invariant, state machine entitas Import database, HTTP framework
Infrastructure Implementasi repository, koneksi DB, integrasi eksternal Business logic, routing

Strict vs Relaxed Layering #

Ada dua varian Layered Architecture yang perlu dipahami:

Strict Layering: Setiap layer hanya boleh memanggil layer yang tepat satu tingkat di bawahnya. Presentation hanya memanggil Application, Application hanya memanggil Domain.

Relaxed Layering: Layer boleh melewati layer di antaranya jika diperlukan — misalnya Presentation boleh langsung memanggil Domain untuk query sederhana.

flowchart LR
    subgraph STRICT["Strict Layering"]
        SP["Presentation"] --> SA["Application"] --> SD["Domain"] --> SI["Infrastructure"]
    end

    subgraph RELAXED["Relaxed Layering"]
        RP["Presentation"] --> RA["Application"]
        RP -->|"boleh langsung"| RD["Domain"]
        RA --> RD --> RI["Infrastructure"]
    end

Untuk kebanyakan sistem, strict layering lebih aman karena mencegah coupling yang tidak terduga. Relaxed layering bisa berguna untuk query read-only sederhana, tapi harus diterapkan dengan konsisten dan documented dengan jelas.


Implementasi Go: Employee Management System #

Contoh berikut mengimplementasikan sistem manajemen karyawan menggunakan Layered Architecture empat lapisan:

Domain Layer #

// domain/employee.go
package domain

import (
	"errors"
	"regexp"
	"time"
)

type EmployeeID string
type DepartmentID string

// Employee adalah entity dengan business rules
// ✓ Tidak ada import dari database, HTTP, atau framework apapun
type Employee struct {
	id           EmployeeID
	fullName     string
	email        string
	departmentID DepartmentID
	salary       int64 // dalam satuan terkecil (sen/rupiah)
	joinDate     time.Time
	isActive     bool
}

var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)

// NewEmployee menegakkan invariant bisnis saat pembuatan
func NewEmployee(fullName, email string, departmentID DepartmentID, salary int64) (*Employee, error) {
	if fullName == "" {
		return nil, errors.New("nama karyawan tidak boleh kosong")
	}
	if !emailRegex.MatchString(email) {
		return nil, errors.New("format email tidak valid")
	}
	if salary < 0 {
		return nil, errors.New("gaji tidak boleh negatif")
	}

	return &Employee{
		id:           EmployeeID(generateID()),
		fullName:     fullName,
		email:        email,
		departmentID: departmentID,
		salary:       salary,
		joinDate:     time.Now(),
		isActive:     true,
	}, nil
}

// Promote menaikkan gaji dengan validasi bisnis
func (e *Employee) Promote(newSalary int64) error {
	if newSalary <= e.salary {
		return errors.New("gaji baru harus lebih besar dari gaji saat ini")
	}
	e.salary = newSalary
	return nil
}

// Resign menonaktifkan karyawan
func (e *Employee) Resign() error {
	if !e.isActive {
		return errors.New("karyawan sudah tidak aktif")
	}
	e.isActive = false
	return nil
}

// Transfer memindahkan karyawan ke departemen lain
func (e *Employee) Transfer(newDeptID DepartmentID) error {
	if e.departmentID == newDeptID {
		return errors.New("karyawan sudah berada di departemen tersebut")
	}
	e.departmentID = newDeptID
	return nil
}

// Getter
func (e *Employee) ID() EmployeeID           { return e.id }
func (e *Employee) FullName() string          { return e.fullName }
func (e *Employee) Email() string             { return e.email }
func (e *Employee) DepartmentID() DepartmentID { return e.departmentID }
func (e *Employee) Salary() int64             { return e.salary }
func (e *Employee) JoinDate() time.Time       { return e.joinDate }
func (e *Employee) IsActive() bool            { return e.isActive }

// EmployeeRepository adalah interface yang didefinisikan di domain
// ✓ Domain mendefinisikan kebutuhan — infrastructure mengimplementasikan
type EmployeeRepository interface {
	Save(employee *Employee) error
	FindByID(id EmployeeID) (*Employee, error)
	FindByDepartment(deptID DepartmentID) ([]*Employee, error)
	FindAllActive() ([]*Employee, error)
}

Application Layer #

// application/employee_service.go
package application

import (
	"errors"
	"fmt"

	"myapp/domain"
)

// EmployeeService mengorkestrasikan use case karyawan
// ✓ Tidak ada detail HTTP atau database di sini — hanya alur bisnis
type EmployeeService struct {
	employeeRepo domain.EmployeeRepository
}

func NewEmployeeService(employeeRepo domain.EmployeeRepository) *EmployeeService {
	return &EmployeeService{employeeRepo: employeeRepo}
}

// CreateEmployeeInput adalah DTO input untuk pembuatan karyawan
type CreateEmployeeInput struct {
	FullName     string
	Email        string
	DepartmentID string
	Salary       int64
}

// EmployeeOutput adalah DTO output yang digunakan untuk semua response
type EmployeeOutput struct {
	ID           string
	FullName     string
	Email        string
	DepartmentID string
	Salary       int64
	JoinDate     string
	IsActive     bool
}

func toOutput(e *domain.Employee) *EmployeeOutput {
	return &EmployeeOutput{
		ID:           string(e.ID()),
		FullName:     e.FullName(),
		Email:        e.Email(),
		DepartmentID: string(e.DepartmentID()),
		Salary:       e.Salary(),
		JoinDate:     e.JoinDate().Format("2006-01-02"),
		IsActive:     e.IsActive(),
	}
}

// CreateEmployee membuat karyawan baru
func (s *EmployeeService) CreateEmployee(input CreateEmployeeInput) (*EmployeeOutput, error) {
	employee, err := domain.NewEmployee(
		input.FullName,
		input.Email,
		domain.DepartmentID(input.DepartmentID),
		input.Salary,
	)
	if err != nil {
		return nil, err
	}

	if err := s.employeeRepo.Save(employee); err != nil {
		return nil, fmt.Errorf("gagal menyimpan karyawan: %w", err)
	}

	return toOutput(employee), nil
}

// PromoteEmployee menaikkan gaji karyawan
func (s *EmployeeService) PromoteEmployee(employeeID string, newSalary int64) (*EmployeeOutput, error) {
	employee, err := s.employeeRepo.FindByID(domain.EmployeeID(employeeID))
	if err != nil {
		return nil, err
	}

	// Delegasikan ke domain entity — service tidak mengandung business logic
	if err := employee.Promote(newSalary); err != nil {
		return nil, err
	}

	if err := s.employeeRepo.Save(employee); err != nil {
		return nil, fmt.Errorf("gagal menyimpan perubahan: %w", err)
	}

	return toOutput(employee), nil
}

// ResignEmployee memproses pengunduran diri karyawan
func (s *EmployeeService) ResignEmployee(employeeID string) error {
	employee, err := s.employeeRepo.FindByID(domain.EmployeeID(employeeID))
	if err != nil {
		return err
	}

	if err := employee.Resign(); err != nil {
		return err
	}

	return s.employeeRepo.Save(employee)
}

// TransferEmployee memindahkan karyawan ke departemen lain
func (s *EmployeeService) TransferEmployee(employeeID, newDeptID string) (*EmployeeOutput, error) {
	employee, err := s.employeeRepo.FindByID(domain.EmployeeID(employeeID))
	if err != nil {
		return nil, err
	}

	if err := employee.Transfer(domain.DepartmentID(newDeptID)); err != nil {
		return nil, err
	}

	if err := s.employeeRepo.Save(employee); err != nil {
		return nil, err
	}

	return toOutput(employee), nil
}

// GetDepartmentEmployees mengambil semua karyawan di satu departemen
func (s *EmployeeService) GetDepartmentEmployees(deptID string) ([]*EmployeeOutput, error) {
	employees, err := s.employeeRepo.FindByDepartment(domain.DepartmentID(deptID))
	if err != nil {
		return nil, err
	}

	outputs := make([]*EmployeeOutput, len(employees))
	for i, emp := range employees {
		outputs[i] = toOutput(emp)
	}
	return outputs, nil
}

Presentation Layer #

// presentation/employee_handler.go
package presentation

import (
	"encoding/json"
	"net/http"

	"myapp/application"
)

// EmployeeHandler adalah HTTP handler — presentation layer
// ✓ Hanya menangani parse request, delegate ke service, format response
type EmployeeHandler struct {
	employeeService *application.EmployeeService
}

func NewEmployeeHandler(svc *application.EmployeeService) *EmployeeHandler {
	return &EmployeeHandler{employeeService: svc}
}

type createEmployeeRequest struct {
	FullName     string `json:"full_name"`
	Email        string `json:"email"`
	DepartmentID string `json:"department_id"`
	Salary       int64  `json:"salary"`
}

func (h *EmployeeHandler) CreateEmployee(w http.ResponseWriter, r *http.Request) {
	var req createEmployeeRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "request tidak valid")
		return
	}

	// Validasi format ada di sini — bukan business validation
	if req.FullName == "" || req.Email == "" || req.DepartmentID == "" {
		writeError(w, http.StatusBadRequest, "semua field wajib diisi")
		return
	}

	output, err := h.employeeService.CreateEmployee(application.CreateEmployeeInput{
		FullName:     req.FullName,
		Email:        req.Email,
		DepartmentID: req.DepartmentID,
		Salary:       req.Salary,
	})
	if err != nil {
		writeError(w, http.StatusUnprocessableEntity, err.Error())
		return
	}

	writeJSON(w, http.StatusCreated, output)
}

type promoteRequest struct {
	NewSalary int64 `json:"new_salary"`
}

func (h *EmployeeHandler) PromoteEmployee(w http.ResponseWriter, r *http.Request) {
	employeeID := r.PathValue("id")

	var req promoteRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "request tidak valid")
		return
	}

	output, err := h.employeeService.PromoteEmployee(employeeID, req.NewSalary)
	if err != nil {
		writeError(w, http.StatusUnprocessableEntity, err.Error())
		return
	}

	writeJSON(w, http.StatusOK, output)
}

func (h *EmployeeHandler) ResignEmployee(w http.ResponseWriter, r *http.Request) {
	employeeID := r.PathValue("id")

	if err := h.employeeService.ResignEmployee(employeeID); err != nil {
		writeError(w, http.StatusUnprocessableEntity, err.Error())
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

func (h *EmployeeHandler) GetDepartmentEmployees(w http.ResponseWriter, r *http.Request) {
	deptID := r.PathValue("dept_id")

	outputs, err := h.employeeService.GetDepartmentEmployees(deptID)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "gagal mengambil data karyawan")
		return
	}

	writeJSON(w, http.StatusOK, outputs)
}

func writeJSON(w http.ResponseWriter, status int, data any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, message string) {
	writeJSON(w, status, map[string]string{"error": message})
}

Infrastructure Layer #

// infrastructure/postgres_employee_repository.go
package infrastructure

import (
	"database/sql"
	"fmt"
	"time"

	"myapp/domain"
)

// PostgresEmployeeRepository mengimplementasikan domain.EmployeeRepository
type PostgresEmployeeRepository struct {
	db *sql.DB
}

var _ domain.EmployeeRepository = (*PostgresEmployeeRepository)(nil)

func NewPostgresEmployeeRepository(db *sql.DB) *PostgresEmployeeRepository {
	return &PostgresEmployeeRepository{db: db}
}

// employeeRow adalah model database — terpisah dari domain entity
type employeeRow struct {
	ID           string
	FullName     string
	Email        string
	DepartmentID string
	Salary       int64
	JoinDate     time.Time
	IsActive     bool
}

func (r *employeeRow) toDomain() *domain.Employee {
	return domain.ReconstructEmployee(
		domain.EmployeeID(r.ID),
		r.FullName,
		r.Email,
		domain.DepartmentID(r.DepartmentID),
		r.Salary,
		r.JoinDate,
		r.IsActive,
	)
}

func (r *PostgresEmployeeRepository) Save(emp *domain.Employee) error {
	_, err := r.db.Exec(
		`INSERT INTO employees (id, full_name, email, department_id, salary, join_date, is_active)
		 VALUES ($1, $2, $3, $4, $5, $6, $7)
		 ON CONFLICT (id) DO UPDATE
		 SET full_name = $2, email = $3, department_id = $4,
		     salary = $5, is_active = $7`,
		string(emp.ID()), emp.FullName(), emp.Email(),
		string(emp.DepartmentID()), emp.Salary(),
		emp.JoinDate(), emp.IsActive(),
	)
	if err != nil {
		return fmt.Errorf("gagal menyimpan employee: %w", err)
	}
	return nil
}

func (r *PostgresEmployeeRepository) FindByID(id domain.EmployeeID) (*domain.Employee, error) {
	row := &employeeRow{}
	err := r.db.QueryRow(
		`SELECT id, full_name, email, department_id, salary, join_date, is_active
		 FROM employees WHERE id = $1`,
		string(id),
	).Scan(
		&row.ID, &row.FullName, &row.Email,
		&row.DepartmentID, &row.Salary,
		&row.JoinDate, &row.IsActive,
	)
	if err == sql.ErrNoRows {
		return nil, fmt.Errorf("karyawan tidak ditemukan: %s", id)
	}
	if err != nil {
		return nil, fmt.Errorf("gagal membaca employee: %w", err)
	}
	return row.toDomain(), nil
}

func (r *PostgresEmployeeRepository) FindByDepartment(deptID domain.DepartmentID) ([]*domain.Employee, error) {
	rows, err := r.db.Query(
		`SELECT id, full_name, email, department_id, salary, join_date, is_active
		 FROM employees WHERE department_id = $1 AND is_active = TRUE`,
		string(deptID),
	)
	if err != nil {
		return nil, fmt.Errorf("gagal query employees: %w", err)
	}
	defer rows.Close()

	var employees []*domain.Employee
	for rows.Next() {
		row := &employeeRow{}
		if err := rows.Scan(
			&row.ID, &row.FullName, &row.Email,
			&row.DepartmentID, &row.Salary,
			&row.JoinDate, &row.IsActive,
		); err != nil {
			return nil, err
		}
		employees = append(employees, row.toDomain())
	}
	return employees, nil
}

func (r *PostgresEmployeeRepository) FindAllActive() ([]*domain.Employee, error) {
	// implementasi serupa FindByDepartment...
	return nil, nil
}

Masalah Tersembunyi: Anemic Domain Model #

Layered Architecture punya jebakan khas yang perlahan merusak maintainability: ketika business logic yang seharusnya ada di domain mulai “merayap” ke service layer, entity menjadi anemic — hanya wadah data tanpa perilaku.

// ✗ Anemic Domain Model — entity hanya data, semua logic di service
type Employee struct { // hanya getter/setter
    ID       string
    FullName string
    Salary   int64
    IsActive bool
}

type EmployeeService struct {
    repo EmployeeRepository
}

func (s *EmployeeService) Promote(id string, newSalary int64) error {
    emp, _ := s.repo.FindByID(id)
    // ✗ validasi bisnis bocor ke service layer
    if newSalary <= emp.Salary {
        return errors.New("gaji baru harus lebih besar")
    }
    emp.Salary = newSalary // ✗ mutasi langsung dari luar entity
    return s.repo.Save(emp)
}

// ✓ Rich Domain Model — business rules ada di dalam entity
type Employee struct {
    id     EmployeeID
    salary int64
    // ... field lain
}

func (e *Employee) Promote(newSalary int64) error {
    // ✓ validasi bisnis di dalam entity
    if newSalary <= e.salary {
        return errors.New("gaji baru harus lebih besar dari gaji saat ini")
    }
    e.salary = newSalary // ✓ entity yang mengontrol state-nya sendiri
    return nil
}

// Service hanya mengorkestrasikan
func (s *EmployeeService) PromoteEmployee(id string, newSalary int64) error {
    emp, _ := s.repo.FindByID(EmployeeID(id))
    if err := emp.Promote(newSalary); err != nil { // ✓ delegate ke entity
        return err
    }
    return s.repo.Save(emp)
}

Database sebagai Pusat Gravitasi #

Salah satu masalah tersembunyi lain dari Layered Architecture yang tidak disiplin adalah ketika database schema menjadi “pusat gravitasi” yang mendiktekan bentuk seluruh codebase:

flowchart TD
    subgraph WRONG["❌ Database-Driven — schema mendiktekan segalanya"]
        DB1[(Schema DB)] -->|"langsung jadi entity"| E1["User struct\n(sesuai kolom DB)"]
        E1 -->|"langsung dikembalikan"| S1["UserService"]
        S1 -->|"langsung di-serialize"| H1["HTTP Response\n(= kolom DB)"]
    end

    subgraph RIGHT["✓ Domain-Driven dalam Layered — domain mendefinisikan segalanya"]
        DOM["User entity\n(berdasarkan business rules)"]
        DB2[(Schema DB)] -->|"mapper"| DOM
        DOM -->|"diproses"| SVC["UserService"]
        SVC -->|"DTO mapper"| RESP["HTTP Response\n(sesuai kebutuhan client)"]
    end

Ketika schema database langsung menjadi entity domain, setiap perubahan kolom di database membutuhkan perubahan di seluruh stack. Dengan mapper yang memisahkan persistence model dari domain entity, perubahan database terlokalisir di infrastructure layer.


Struktur Direktori #

myapp/
├── presentation/              ← Presentation layer
│   ├── employee_handler.go    ← HTTP handler
│   ├── middleware.go          ← Auth, logging, cors
│   └── router.go              ← Route registration
│
├── application/               ← Application layer
│   ├── employee_service.go    ← Use case orchestration
│   └── dto.go                 ← Input/output DTO
│
├── domain/                    ← Domain layer
│   ├── employee.go            ← Entity + business rules
│   ├── department.go
│   └── repository.go          ← Repository interfaces
│
├── infrastructure/            ← Infrastructure layer
│   ├── postgres_employee_repository.go
│   ├── redis_cache.go
│   └── smtp_mailer.go
│
└── cmd/
    └── main.go                ← Wiring semua dependency

Layered vs Clean Architecture #

Ini adalah pertanyaan yang paling sering muncul ketika tim mempertimbangkan Layered Architecture:

Aspek Layered Architecture Clean Architecture
Arah dependency Top-down (presentation → domain → infra) Selalu ke dalam (infra → adapters → use cases → entities)
Posisi database Infrastructure layer, tapi domain sering coupling ke DB Detail implementasi yang bisa diganti kapan saja
Testability Baik jika menggunakan interface Sangat baik — domain tidak butuh DB untuk diuji
Learning curve Rendah — familiar untuk sebagian besar developer Lebih tinggi — banyak layer dan interface
Cocok untuk Sistem menengah, tim yang baru kenal arsitektur Sistem kompleks dengan umur panjang
Overhead Rendah — cukup 4 folder Lebih tinggi — banyak interface dan mapping
Risiko Anemic domain model, database coupling Over-engineering untuk sistem kecil

Layered Architecture adalah pintu masuk yang baik ke arsitektur yang lebih mature. Banyak tim memulai dengan Layered dan berevolusi ke Clean atau Hexagonal saat complexity meningkat — dan transisi ini lebih mudah jika Layered Architecture diterapkan dengan disiplin sejak awal.


Anti-Pattern yang Harus Dihindari #

// ✗ Business logic di presentation layer
func (h *EmployeeHandler) Promote(w http.ResponseWriter, r *http.Request) {
	emp := getEmployee(r)
	// ✗ validasi bisnis di HTTP handler
	if emp.Salary >= 10000000 {
		writeError(w, 400, "gaji sudah mencapai batas maksimum")
		return
	}
	emp.Salary += 1000000 // ✗ mutasi langsung di handler
	saveEmployee(emp)
}

// ✓ Handler hanya parse dan delegate
func (h *EmployeeHandler) Promote(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	var req promoteRequest
	json.NewDecoder(r.Body).Decode(&req)
	output, err := h.svc.PromoteEmployee(id, req.NewSalary) // ✓ delegate
	if err != nil { writeError(w, 422, err.Error()); return }
	writeJSON(w, 200, output)
}

// ✗ Skip layer — presentation langsung ke infrastructure
func (h *EmployeeHandler) GetEmployee(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	// ✗ handler langsung query database — skip application dan domain
	var emp employeeRow
	db.QueryRow("SELECT * FROM employees WHERE id = $1", id).Scan(&emp)
	json.NewEncoder(w).Encode(emp)
}

// ✓ Alur selalu melalui layer yang benar
func (h *EmployeeHandler) GetEmployee(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	output, err := h.svc.GetEmployee(id) // ✓ lewat application service
	if err != nil { writeError(w, 404, err.Error()); return }
	writeJSON(w, 200, output)
}

// ✗ Circular dependency — domain mengimport dari application
package domain // ✗

import "myapp/application" // JANGAN — dependency cycle!

// ✓ Dependency selalu satu arah ke bawah
// presentation imports application
// application imports domain
// domain tidak import apapun dari layer atas

Kapan Layered Architecture Cukup, Kapan Perlu Berevolusi #

Layered Architecture sangat cocok jika:
  ✓ Sistem berskala kecil hingga menengah (1–15 developer)
  ✓ Domain bisnis tidak terlalu kompleks — CRUD dengan beberapa aturan bisnis
  ✓ Tim baru dalam arsitektur — learning curve rendah membantu onboarding
  ✓ Digunakan sebagai internal architecture microservice individual
  ✓ Waktu development cepat menjadi prioritas

Pertimbangkan Clean / Hexagonal / Onion jika:
  ✗ Business rules sangat kompleks dan terus berkembang
  ✗ Banyak integrasi eksternal yang mungkin diganti
  ✗ Testability menjadi prioritas — butuh unit test tanpa database
  ✗ Tim besar dengan banyak developer bekerja paralel
  ✗ Anemic domain model sudah mulai terasa — service terus membengkak

Tanda-tanda Layered Architecture perlu diperketat atau dievolusi:
  □ Service layer lebih panjang dari 500 baris per file
  □ Entity hanya berisi getter/setter tanpa method bisnis
  □ Presenter/handler mengandung kondisional bisnis
  □ Unit test butuh database nyata untuk berjalan
  □ Perubahan schema database menyebabkan banyak file berubah

Checklist Review Layered Architecture #

PRESENTATION LAYER:
  □ Handler hanya parse request dan format response
  □ Tidak ada business logic (conditional bisnis, kalkulasi) di handler
  □ Validasi format (required field, tipe data) boleh di sini — tapi bukan business rule
  □ HTTP status code dipilih berdasarkan jenis error dari service

APPLICATION LAYER:
  □ Service mengorkestrasikan use case — bukan mengandung business logic
  □ Business logic ada di domain entity, bukan di service
  □ DTO (Input/Output) digunakan untuk komunikasi antar layer
  □ Service bergantung pada domain repository interface, bukan concrete implementation

DOMAIN LAYER:
  □ Tidak ada import dari library eksternal (database, HTTP, cache)
  □ Entity memiliki method yang menegakkan business rules dan invariant
  □ Repository interface didefinisikan di domain layer
  □ Entity menggunakan private field dengan public getter yang terkontrol

INFRASTRUCTURE LAYER:
  □ Repository implementation menggunakan persistence model terpisah dari domain entity
  □ Ada mapper: persistence model → domain entity dan sebaliknya
  □ Compile-time interface check: var _ domain.Repo = (*ImplRepo)(nil)
  □ Tidak ada business logic di repository

DEPENDENCY:
  □ Tidak ada circular dependency antar layer
  □ Tidak ada layer yang skip ke layer yang bukan tepat di bawahnya (kecuali relaxed yang documented)
  □ Semua dependency ke external system melalui interface

PENGUJIAN:
  □ Domain entity diuji murni tanpa dependency apapun
  □ Application service diuji dengan mock repository
  □ Repository diuji dengan integration test
  □ go test -race dijalankan secara rutin

Ringkasan #

  • Layered Architecture adalah pola paling klasik dan paling mudah dipahami — empat layer dengan tanggung jawab yang jelas: presentation, application, domain, dan infrastructure; dependency selalu mengalir ke bawah.
  • Setiap layer punya satu tanggung jawab — handler parse request, service orkestrasi, domain mengandung business rules, infrastructure berinteraksi dengan teknologi eksternal; jangan campur tanggung jawab.
  • Jaga business logic di domain layer — jika validasi dan kalkulasi bisnis mulai merayap ke service atau handler, itu tanda anemic domain model yang perlu segera diperbaiki.
  • Repository interface di domain layer — domain mendefinisikan kebutuhannya melalui interface, infrastructure mengimplementasikannya; ini memungkinkan database bisa diganti tanpa menyentuh domain.
  • Pisahkan persistence model dari domain entity — ORM annotation dan JSON tag tidak boleh ada di domain entity; gunakan mapper untuk konversi antara keduanya.
  • Handler tipis adalah tanda kesehatan arsitektur — jika handler lebih dari 30 baris, ada kemungkinan business logic yang seharusnya di service atau domain sudah bocor ke presentation.
  • Strict layering mencegah coupling tersembunyi — jangan biarkan presentation layer skip langsung ke infrastructure; setiap layer harus melewati layer yang tepat di bawahnya.
  • Layered Architecture adalah pintu masuk ke arsitektur yang lebih mature — ketika business rules makin kompleks atau testability menjadi prioritas, berevolusi ke Clean atau Hexagonal Architecture menjadi lebih mudah jika Layered sudah diterapkan dengan disiplin.
  • Anemic domain model adalah anti-pattern paling umum — entity yang hanya berisi getter/setter adalah tanda bahwa arsitektur tidak memanfaatkan kekuatan OOP; domain harus mengandung behavior, bukan hanya data.
  • Cocok untuk tim yang baru kenal arsitektur — learning curve rendah dan struktur yang familier membantu onboarding dan konsistensi; setelah tim mature, pertimbangkan untuk berevolusi sesuai kebutuhan sistem.

← Sebelumnya: Onion Architecture   Berikutnya: Monolithic Architecture →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact