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 →