Spring Boot & Layered Architecture
Bayangkan Anda diminta membangun aplikasi backend dari nol. Tanpa framework, Anda harus mengurus sendiri server HTTP, koneksi database, parsing JSON, sampai manajemen koneksi — semua dari awal. Di dunia Java, ada dua pemecah masalah yang bekerja sama: Spring Framework menyediakan fondasi (dependency injection, transaction, database access), dan Spring Boot menyederhanakannya sehingga Anda bisa fokus ke logika bisnis. Artikel ini membahas keduanya, ditambah pola arsitektur berlapis (layered architecture) yang paling umum dipakai untuk menyusun kode backend yang rapi.
Apa Itu Spring Boot?
Spring Boot adalah framework Java untuk membangun aplikasi yang siap produksi (production-ready) dengan konfigurasi minimal. Singkatnya, Spring Boot adalah "pintu cepat" menuju ekosistem Spring: Anda menulis kelas utama dengan main(), menjalankannya, dan tiba-tiba aplikasi web Anda sudah berjalan.
Apa yang membuat Spring Boot istimewa? Ada tiga fitur kunci:
- Auto-configuration — Spring Boot menebak konfigurasi berdasarkan dependency yang ada di
classpath. Tambahkan dependency database, dan ia otomatis menyiapkan connection pool. Tambahkan dependency web, dan ia otomatis menyiapkan server HTTP. Detailnya dijelaskan di dokumentasi auto-configuration. - Starter dependencies — kumpulan dependency yang sudah dipaketkan. Satu baris
spring-boot-starter-webmembawa puluhan library web yang saling kompatibel, tanpa Anda perlu menebak versi yang cocok. - Embedded server — server web (Tomcat, Jetty, atau Undertow) ditanam langsung ke dalam aplikasi. Hasilnya, aplikasi Anda berbentuk satu file
.jaryang bisa dijalankan denganjava -jar app.jar; tidak perlu install server terpisah.
Spring Boot juga memiliki kelas SpringApplication sebagai titik masuk. Menurut dokumentasi resminya, SpringApplication.run() melakukan bootstrapping: memulai aplikasi, menyiapkan ApplicationContext, menjalankan auto-configuration, dan akhirnya menjalankan server.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
}
Spring Framework vs Spring Boot
Banyak junior engineer bingung antara keduanya. Secara sederhana: Spring Framework adalah fondasinya, Spring Boot adalah kemasannya.
| Aspek | Spring Framework | Spring Boot |
|---|---|---|
| Peran | Fondasi: DI container, MVC, transaction, data access | Pengemas Spring agar cepat dipakai |
| Konfigurasi | Banyak @Configuration + XML manual | Auto-configuration, minimal konfigurasi |
| Server web | Perlu setup manual (misal deploy ke Tomcat) | Embedded server, langsung jalan |
| Versi dependency | Anda yang mengatur | Starter mengatur versi yang kompatibel |
| Waktu setup proyek | Lama, banyak boilerplate | Detik, lewat Spring Initializr |
| Titik masuk | Banyak web.xml / servlet config | Satu kelas @SpringBootApplication |
Intuisi cepat: Spring Framework ibarat mesin mobil yang lengkap dan hebat; Spring Boot ibarat membeli mobil yang sudah jadi — tinggal kunci kontak dan melaju.
Kenapa Layered Architecture?
Sebuah aplikasi akan terus bertumbuh: makin banyak endpoint, makin banyak aturan bisnis, makin banyak tabel di database. Kalau semua kode ditumpuk di satu file, dalam hitungan bulan file itu menjadi raksasa yang mustahil dipelihara. Di sinilah layered architecture (arsitektur berlapis) berperan.
Layered architecture adalah cara menyusun kode dengan memisahkan perhatian (separation of concerns): setiap lapisan punya satu tanggung jawab, dan lapisan tersebut berbicara hanya kepada lapisan di sebelahnya. Tiga keuntungan utamanya:
- Testability — setiap lapisan bisa diuji terpisah. Anda bisa menguji service tanpa perlu server HTTP berjalan.
- Maintainability — perubahan di satu lapisan tidak merembet ke mana-mana. Ganti dari MySQL ke PostgreSQL? Cukup sentuh bagian repository.
- Readability — siapa pun yang membaca kode langsung tahu: "kalau soal data, lihat repository; kalau soal aturan bisnis, lihat service."
Lapisan ini punya aturan arah dependensi: lapisan atas boleh memakai lapisan bawah, tetapi tidak sebaliknya. Controller memanggil Service; Service memanggil Repository; Repository berhadapan dengan database. Arah yang benar membuat kode mengalir seperti arus air: dari atas ke bawah, dan tidak pernah melompat atau berbalik.
Empat Lapisan Inti
Dalam praktik sehari-hari di Spring Boot, arsitektur berlapis diwujudkan dalam empat lapisan. Setiap lapisan adalah sekumpulan kelas yang ditandai dengan anotasi khas, supaya Spring (dan pembaca kode) tahu perannya.
| Lapisan | Anotasi khas | Tanggung jawab | Apa yang TIDAK boleh dilakukan |
|---|---|---|---|
| Controller | @RestController | Terima HTTP request, validasi dasar, delegasi ke service, kembalikan respons | Menulis query atau logika bisnis |
| Service | @Service | Logika bisnis, transaksi, koordinasi antar repository | Menulis SQL langsung |
| Repository | @Repository | Akses database, map tabel → objek | Berisi aturan bisnis |
| Entity + DB | @Entity | Representasi tabel database sebagai objek Java | Logika apa pun |
1. Controller — Presentation Layer
Controller adalah "pintu masuk" aplikasi: ia berbicara bahasa HTTP (request dan response). Tugasnya hanya tiga: menerima request, mengecek input pada level paling dasar, lalu menyerahkan pekerjaan ke service. Jawaban dikembalikan sebagai JSON — Spring menangani serialisasi objek Java ke JSON secara otomatis.
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}
Rule of thumb: controller harus setipis mungkin (thin controller). Kalau controller Anda mulai berisi loop, perhitungan, atau
ifyang rumit, berarti logika itu seharusnya tinggal di service.
2. Service — Business Logic Layer
Service adalah "jantung" aplikasi. Di sinilah aturan bisnis hidup: "pelanggan tidak boleh memesan barang stok nol", "saldo tidak boleh minus", "email harus unik". Service juga yang mengatur transaksi — sekumpulan operasi database yang harus sukses semuanya atau gagal semuanya.
Anotasi @Transactional memberi tahu Spring untuk membungkus metode dalam sebuah transaksi. Jika terjadi exception di tengah jalan, Spring membatalkan (rollback) semua perubahan yang sudah dilakukan. Detailnya bisa Anda pelajari kembali di materi dasar database relasional.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional(readOnly = true)
public UserDto getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User dengan id " + id + " tidak ditemukan"));
return new UserDto(user.getId(), user.getName(), user.getEmail());
}
}
Perhatikan pola di atas: service tidak tahu menahu soal HTTP — ia tidak melihat @GetMapping atau ResponseEntity. Ia hanya bekerja dengan objek Java dan melempar exception ketika ada yang salah. Siapa yang menerjemahkan exception menjadi status code HTTP? Itu urusan presentation layer, dan biasanya ditangani oleh @RestControllerAdvice.
3. Repository — Data Access Layer
Repository adalah "penghubung ke database". Di Spring Boot dengan Spring Data JPA, Anda cukup membuat interface yang mewarisi JpaRepository — Spring otomatis menyediakan implementasinya di runtime. Anda tidak menulis implementasi manual sama sekali.
Keajaiban lainnya adalah derived query: nama metode dibaca Spring lalu diterjemahkan menjadi query. findByEmail(String email) akan menjadi "SELECT ... WHERE email = ?" tanpa Anda menulis satu baris SQL pun.
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}
4. Entity & Database
Entity adalah kelas Java yang mewakili satu tabel di database. Setiap instance adalah satu baris (record), setiap field adalah satu kolom. Anotasi JPA (@Entity, @Table, @Column) menghubungkan dunia Java dengan dunia relasional.
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
protected User() {
}
public User(String email, String name) {
this.email = email;
this.name = name;
}
public Long getId() {
return id;
}
public String getEmail() {
return email;
}
public String getName() {
return name;
}
}
Penting: entity mewakili data, bukan tampilan. Jangan jadikan entity sebagai respons API secara langsung — itulah gunanya DTO (lihat bagian Best Practice).
Dependency Injection
Sekarang perhatikan pola yang berulang di semua kode di atas: constructor menerima parameter (UserService dan UserRepository), tapi tidak ada satu pun new UserService(...) yang Anda tulis. Siapa yang membuat objek-objek itu dan menyambungkannya? Jawabannya: Dependency Injection (DI).
Dependency Injection adalah pola di mana sebuah objek tidak membuat dependensinya sendiri, melainkan diberikan (injected) oleh container. Objek yang dikelola Spring ini disebut bean. Kumpulan semua bean hidup dalam ApplicationContext — semacam "lemari penyimpanan" Spring yang menciptakan, menghubungkan, dan mengelola siklus hidup setiap bean.
Kenapa DI dibutuhkan? Dua alasan utama:
- Decoupling —
UserControllertidak perlu tahu bagaimanaUserServicedibangun. Ia hanya perlu tahu kontraknya (metode apa yang tersedia). Ini membuat komponen saling lepas dan mudah ditukar. - Mudah di-test — dalam unit test, Anda bisa menyuntikkan mock (tiruan) repository sehingga test tidak menyentuh database sungguhan.
Spring mendeteksi bean lewat anotasi stereotipe pada kelas. Tiap anotasi sekaligus memberi tahu pembaca kode peran kelas tersebut:
| Anotasi | Peran | Mirip peran |
|---|---|---|
@Component | Bean generik | Komponen netral |
@Service | Bean berisi logika bisnis | @Component untuk layer service |
@Repository | Bean akses data (plus exception translation) | @Component untuk layer data |
@Controller / @RestController | Bean yang menangani HTTP request | @Component untuk layer presentasi |
Ada dua gaya injeksi yang umum: constructor injection (parameternya ditaruh di constructor) dan field injection dengan @Autowired di atas field.
// Constructor injection — paling disarankan (immutable, jelas, mudah di-test)
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
// Field injection — cepat ditulis, tapi lebih sulit di-test
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
Konsensus komunitas: untuk komponen wajib, gunakan constructor injection. Field tetap bisa dideklarasikan
final, sehingga kontraknya jelas dan tidak bisa diubah di kemudian hari. Penjelasan resminya ada di dokumentasi dependencies Spring.
Alur Request Lengkap
Klien (browser / mobile)
│
│ 1. HTTP GET /api/users/42
▼
┌─────────────────────┐
│ DispatcherServlet │ (front controller — pintu masuk semua request)
└──────────┬──────────┘
│ 2. cari handler yang cocok
▼
┌─────────────────────┐
│ UserController │ (presentation layer, @GetMapping)
└──────────┬──────────┘
│ 3. delegasi, userService.getUserById(42)
▼
┌─────────────────────┐
│ UserService │ (business logic + @Transactional)
└──────────┬──────────┘
│ 4. userRepository.findById(42)
▼
┌─────────────────────┐
│ UserRepository │ (data access layer)
└──────────┬──────────┘
│ 5. SELECT ... WHERE id = 42
▼
┌─────────────────────┐
│ Database │
└──────────┬──────────┘
│ 6. data dikembalikan naik sebagai objek Java
▼
DispatcherServlet → 7. objek diserialisasi → JSON
│
│ 8. HTTP 200 OK + JSON body
▼
Klien menerima respons
Penjelasan tiap langkah:
- HTTP request tiba —
GET /api/users/42masuk dari klien. - DispatcherServlet — servlet ini bertindak sebagai front controller: menerima semua request, lalu mencari handler (metode
@GetMapping) yang cocok dengan path dan method. Spring Boot memasangnya otomatis lewat starter web. - Controller menjalankan tugasnya — validasi minimal, lalu memanggil service dengan id
42. Tidak lebih. - Service memproses logika bisnis — transaksi dibuka, lalu data diminta ke repository. Jika id tidak ketemu, service melempar exception.
- Repository bertanya ke database — Spring Data JPA menjalankan query yang cocok dengan nama metode.
- Data naik kembali — hasil query diubah menjadi objek
User, dikirim naik ke service, lalu dipetakan menjadi DTO. - Serialisasi ke JSON — Spring (lewat Jackson) mengubah objek Java menjadi teks JSON.
- Response dikirim — klien menerima
200 OKdengan JSON, siap di-render frontend.
Studi Kasus Mini: GET /api/users/{id}
Mari kita bangun endpoint GET /api/users/{id} dari atas ke bawah. Enam file berikut saling terhubung dan membentuk satu alur utuh.
Entity — representasi tabel:
// User.java
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
protected User() {
}
public User(String email, String name) {
this.email = email;
this.name = name;
}
public Long getId() { return id; }
public String getEmail() { return email; }
public String getName() { return name; }
}
Repository — akses data:
// UserRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Exception khusus — agar error bisa dibedakan dari error lain:
// UserNotFoundException.java
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
Service — logika bisnis + aturan transaksi:
// UserService.java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional(readOnly = true)
public UserDto getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(
"User dengan id " + id + " tidak ditemukan"));
return new UserDto(user.getId(), user.getName(), user.getEmail());
}
}
DTO — objek respons yang tidak mengekspos semua field entity:
// UserDto.java
public record UserDto(Long id, String name, String email) {
}
Controller — pintu masuk HTTP:
// UserController.java
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}
Exception handler — menerjemahkan UserNotFoundException menjadi 404 Not Found:
// GlobalExceptionHandler.java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}
Alurnya: GET /api/users/42 → UserController.getUser(42) → UserService.getUserById(42) → UserRepository.findById(42) → database. Data kembali naik, dipetakan ke UserDto, dan dikirim sebagai JSON. Kalau id 42 tidak ada, repository mengembalikan Optional.empty(), service melempar UserNotFoundException, dan handler mengubahnya menjadi respons 404. Satu alur, lima lapisan, semua terpisah rapi.
Best Practice
- Controller tetap tipis — controller hanya mengarahkan lalu lintas HTTP, tidak berisi logika bisnis.
- Service berisi logika — semua keputusan bisnis, validasi level bisnis, dan koordinasi antar repository hidup di service.
- Jangan query di controller — akses data harus selalu lewat repository. Ini menjaga konsistensi dan memudahkan testing.
- Gunakan DTO untuk respons — jangan mengirim entity langsung ke klien. Entity bisa memuat field sensitif (misal
passwordHash) atau relasi yang tidak perlu diekspos. DTO juga membuat kontrak API stabil meski struktur tabel berubah. - Penamaan konsisten — pola
User(entity),UserRepository,UserService,UserController,UserDtomemudahkan siapa pun menavigasi proyek. - Gunakan constructor injection — hindari
@Autowiredfield untuk komponen wajib.
Ringkasan
Spring Boot menyederhanakan ekosistem Spring dengan auto-configuration, starter dependencies, dan embedded server sehingga Anda bisa fokus pada logika bisnis (dokumentasi Spring Boot). Kode aplikasi disusun dalam layered architecture dengan aturan arah dependensi yang ketat: Controller hanya menerima HTTP dan mendelegasikan; Service memuat aturan bisnis dan transaksi; Repository menangani akses data; Entity merepresentasikan tabel. Semua lapisan disambung oleh dependency injection — Spring menciptakan bean, menyimpannya di ApplicationContext, dan menyuntikkannya ke objek yang membutuhkan. Hasilnya: kode yang mudah diuji, mudah diubah, dan mudah dipahami.
Lanjut membaca
- Spring Boot Documentation — dokumentasi resmi seluruh Spring Boot
- Using the @SpringBootApplication Annotation — titik masuk aplikasi Spring Boot
- Auto-configuration — cara Spring Boot menebak konfigurasi
- Dependencies (Constructor Injection) — dasar dependency injection di Spring
- Sebelumnya: Anatomi Protokol HTTP & Siklus Request-Response
- Selanjutnya: Dasar Database Relasional & SQL — memahami lapisan paling bawah: bagaimana data disimpan secara permanen