Spring Boot ile Sonsuz Kaydırma (Infinite Scroll) Anatomisi: Büyük Veride Performans Sırrı
Bir e-ticaret platformunda veya sosyal medya akışında aşağı doğru indikçe ürünlerin veya gönderilerin yağ gibi akıp gittiğini, sayfa…
Spring Boot ile Sonsuz Kaydırma (Infinite Scroll) Anatomisi: Büyük Veride Performans Sırrı
Bir e-ticaret platformunda veya sosyal medya akışında aşağı doğru indikçe ürünlerin veya gönderilerin yağ gibi akıp gittiğini, sayfa yenilenmeden saniyeler içinde yeni verilerin yüklendiğini fark etmişsinizdir. Geleneksel web uygulamalarında listeleme denince akla ilk olarak Pagination (Sayfalama) gelir. Peki ya milyonlarca ürünü olan devasa e-ticaret platformları, bu kadar veriyi veritabanını felç etmeden nasıl bu kadar hızlı önümüze getirebiliyor?
Bu yazıda, Frontend ve Backend iş birliği ile hayata geçen Sonsuz Kaydırma (Infinite Scroll / Lazy Loading) yapısının arka plandaki mimari işleyişine odaklanacağız. Bir backend developer gözlüğü takarak, büyük veride sorgu performansını nasıl maksimize edeceğimizi ve veritabanı kilitlenmelerini nasıl engelleyeceğimizi inceleyeceğiz.
🆚 Geleneksel Sayfalama (Offset-Based) vs. Sonsuz Kaydırma (Cursor-Based)
Çoğumuz projelerimizde Spring Data JPA’nın bize sunduğu Pageable ve Page<T> yapılarını kullanarak sayfalama yapıyoruz. Bu yaklaşım arka planda SQL'e LIMIT ve OFFSET anahtar kelimelerini ekler.
Küçük veri setlerinde harika çalışan bu yöntemin, sonsuz kaydırma senaryolarında iki büyük handikapı vardır:
- Performans Darboğazı (O(N)): Siz 1000. sayfaya gelip sıradaki 20 ürünü istediğinizde (
LIMIT 20 OFFSET 20000), veritabanı diskteki ilk 20.000 satırı tek tek okur, hafızaya alır, çöpe atar ve sadece son 20 satırı size döner. Veri büyüdükçe sorgu süresi milisaniyelerden saniyelere çıkar. - Veri Tutarlılığı (Data Drifting) Problemi: Kullanıcı 1. sayfadaki 20 ürünü incelerken sisteme yeni bir ürün eklenirse, tüm veriler bir satır aşağı kayar. Kullanıcı aşağı inip 2. sayfayı istediğinde, 1. sayfanın sonundaki ürünü 2. sayfanın başında tekrar (mükerrer) görür.
İşte bu yüzden sonsuz kaydırma tasarımlarında OFFSET yerine Cursor-Based Pagination (Keyset Pagination) mimarisi tercih edilir.
🧠 Sonsuz Kaydırma Yapısının Mantığı (lastSeenId)
Sonsuz kaydırmanın sırrı, frontend’e sayfa numarası sormak yerine “En son hangi veriyi gördün?” diye sormaktır.
Frontend (Mobil veya Web), backend’den gelen listede ekranın en altında kalan son ürünün benzersiz kimliğini (**lastSeenId*) hafızasında tutar. Kullanıcı aşağı kaydırıp listenin sonuna yaklaştığında, frontend backend'e şu isteği fırlatır: "Kanka, ben en son 105 ID'li ürünü gördüm. Bana bu ID'den sonra gelen sıradaki 20 ürünü gönder."*
Veritabanı, id (Primary Key) üzerinde otomatik olarak indeks (B-Tree Index) tuttuğu için aradaki binlerce eski kaydı hiç okumaz; doğrudan 105 numaralı kayda zıplar ve sıradaki 20 ürünü $O(\log N)$ zaman karmaşıklığıyla, yani ışık hızında çeker.
🛠️ Adım Adım Kod Mimarisi
Gelin bu teorik mantığı Spring Boot ve PostgreSQL üzerinde gerçeğe dönüştürelim.
1. Veritabanı ve Repository Katmanı (SQL & JPQL)
Veritabanı tarafında yazacağımız sorgu, geleneksel sayfalardan bağımsız olarak her zaman id > :lastSeenId şartını aramalıdır.
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("""
SELECT p FROM Product p
WHERE p.category.id = :categoryId AND p.id > :lastSeenId
ORDER BY p.id ASC
""")
List<Product> findNextProducts(Long categoryId, Long lastSeenId, Pageable pageable);
}
2. Servis Katmanı (Business Logic)
Buradaki en kritik mühendislik numarası, PageRequest.of(0, size) kullanımıdır. Sayfa numarası her zaman 0 kalır, çünkü biz veritabanında kaçıncı satırı atlayacağımızı OFFSET ile değil, lastSeenId koordinatıyla dinamik olarak yönetiyoruz. Ayrıca tip güvenliği ve dış dünyaya veri sızdırmamak için veritabanı entity nesnesini bir DTO'ya map'liyoruz.
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
public List<GetProductByCategoryId> getProductByCategoryId(Long categoryId, Long lastSeenId, int size) {
// Sayfa numarası hep 0'dır, limiti sadece adet (size) yönetimi için kullanıyoruz
Pageable limit = PageRequest.of(0, size);
List<Product> products = productRepository.findNextProducts(categoryId, lastSeenId, limit);
return products.stream().map(product -> new GetProductByCategoryId(
product.getId(),
product.getName(),
product.getCategory().getId(),
product.getDescription(),
product.getPrice(),
product.getSku()
)).toList();
}
}
3. Controller Katmanı (API Sözleşmesi)
Kullanıcı kategoriye ilk tıkladığında henüz hiçbir ürün görmediği için lastSeenId değerine varsayılan olarak 0 veriyoruz.
@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping("/infinite-scroll")
public ApiResponse<List<GetProductByCategoryId>> getProducts(
@RequestParam Long categoryId,
@RequestParam(defaultValue = "0") Long lastSeenId, // İlk istekte 0, sonra dinamik gelir
@RequestParam(defaultValue = "20") int size) {
List<GetProductByCategoryId> products = productService.getProductByCategoryId(categoryId, lastSeenId, size);
return ApiResponse.success("Products fetched via cursor", products);
}
}
🎯 Sonuç ve Üretim (Production) Notu
Cursor-Based Pagination kullanarak tasarladığımız bu mimari sayesinde:
- Veritabanımız milyonlarca satır veri içerse bile arama performansımız her zaman sabit ve mikrosaniyeler mertebesinde kalacaktır.
- Kullanıcı aşağı kaydırırken araya yeni ürünler eklense veya silinse dahi, ekranda kayma veya mükerrer veri görme riski tamamen ortadan kalkacaktır.
Küçük bir mimari trade-off: Bu yöntem sonsuz kaydırmalı akışlar (Trendyol ana sayfa, Twitter/Instagram feed) için mükemmeldir; ancak kullanıcıya doğrudan “50. sayfaya atla” seçeneği sunmak istiyorsanız bu yapıyı kullanamazsınız. Tasarım yaparken business ekibinin ihtiyaçlarını göz önünde bulundurmak bu yüzden değerlidir.
Github profilim:
https://github.com/oguzhansecgel
Sorularınız ve Geri Dönüşleriniz için Linkedin Profilim:(Geri dönüşleriniz benim için çok önemli :)
메타데이터
- post_id
- d68e77c53d45
- slug
- spring-boot-ile-sonsuz-kaydırma-infinite-scroll-anatomisi-büyük-veride-performans-sırrı-d68e77c53d45
- url
- https://medium.com/@oguzhan.secgel.19/spring-boot-ile-sonsuz-kayd%C4%B1rma-infinite-scroll-anatomisi-b%C3%BCy%C3%BCk-veride-performans-s%C4%B1rr%C4%B1-d68e77c53d45
- canonical_url
- https://medium.com/@oguzhan.secgel.19/spring-boot-ile-sonsuz-kayd%C4%B1rma-infinite-scroll-anatomisi-b%C3%BCy%C3%BCk-veride-performans-s%C4%B1rr%C4%B1-d68e77c53d45
- author_url
- https://medium.com/@oguzhan.secgel.19
- status
- ok
- fetched_at
- 2026-07-17 07:03:26