Розробка системи підписок на користувачів у мобільному додатку
Відмітимо: коли кількість користувачів перевалює за 500 тисяч, а кількість підписок — за 10 мільйонів, проста таблиця follows без індексу починає гальмувати: підрахунок підписників перетворюється на full scan. Ми — команда з 5-річним досвідом розробки соціальних додатків; на нашому рахунку більше 30 проектів із системами підписок, що витримують мільйонні навантаження. Ми вирішуємо це завдання комплексно — від схеми даних з індексами та денормалізацією до push-повідомлень і рекомендацій. Індекс на followee_id прискорює COUNT(*) у тисячу разів при мільйоні записів, а денормалізовані лічильники в users дають відповідь менше 1 мс. Замовте розробку системи підписок у нас — ми гарантуємо стабільність при будь-якому навантаженні. Наш досвід дозволяє врахувати всі нюанси: App Store Review Guidelines (Section 4.2/5.1), вимоги до конфіденційності та продуктивність. В одному з проектів для соціальної мережі з 2M DAU ми спроектували підсистему, що обробляє 10K дій на секунду без падінь. У цій статті розберемо, яка схема БД оптимальна, як уникнути падіння продуктивності при зростанні, і чому оптимістичне оновлення UI критичне для користувацького досвіду.
Схема даних та запити
Таблиця follows:
CREATE TABLE follows ( follower_id BIGINT NOT NULL, followee_id BIGINT NOT NULL, created_at TIMESTAMP DEFAULT NOW(), PRIMARY KEY (follower_id, followee_id) ); CREATE INDEX idx_follows_followee ON follows (followee_id); Індекс на followee_id обов'язковий — він прискорює підрахунок підписників у 1000 разів при 1M записів. Денормалізація лічильників у таблиці users (поля followers_count та following_count) оновлюється через тригер або чергу, що дає час відповіді менше 1 мс.
| Метод підрахунку | Час виконання | Навантаження на БД |
|---|---|---|
| Денормалізоване поле | <1 ms | Немає |
| SELECT COUNT(*) з індексом | 10–100 ms | Помірне |
Ще один спосіб прискорити перевірку взаємної підписки — кешування в Redis: 100 разів швидше SQL при 1M записів. Для частих запитів (хто підписаний на мене) зберігаємо hash-set follower_id → Set<followee_id>.
Як реалізувати кнопку Follow/Unfollow з оптимістичним оновленням?
Оптимістичне оновлення обов'язкове — кнопка перемикається миттєво до відповіді сервера. При помилці стан відкочується. На iOS використовуємо Combine, на Android — StateFlow. Кроки реалізації:
- Переключити UI-стан кнопки на протилежний (наприклад, з «Підписатися» на «Підписаний»).
- Відправити асинхронний запит follow/unfollow.
- При успіху зафіксувати новий стан.
- При помилці відкотити UI до початкового стану.
// iOS func toggleFollow(userId: String, currentlyFollowing: Bool) { let optimisticState = !currentlyFollowing updateFollowButton(isFollowing: optimisticState) let request = optimisticState ? apiService.follow(userId) : apiService.unfollow(userId) request.sink( receiveCompletion: { [weak self] completion in if case .failure = completion { self?.updateFollowButton(isFollowing: currentlyFollowing) // відкат } }, receiveValue: { _ in } ).store(in: &cancellables) } Три стани кнопки: «Підписатися», «Підписаний» та «Відписатися» (показується при лонг-тапі). Важливо не робити «Відписатися» основним текстом — користувачі можуть сприйняти це за підтвердження підписки. Для SwiftUI використовуємо @State з патерном оптимістичного оновлення, для Jetpack Compose — mutableStateOf.
Закриті акаунти та заявки на підписку
Якщо додаток підтримує закриті профілі, вводиться таблиця follow_requests (requester_id, target_id, status, created_at). Цільовий користувач бачить вхідні заявки, приймає або відхиляє. При прийнятті запис переміщується в follows, і відправнику надходить push-повідомлення.
Як організована пагінація списку підписників?
Пагінація cursor-based по created_at DESC. Для кожного користувача в списку batch-запит перевіряє isFollowedByMe: SELECT followee_id FROM follows WHERE follower_id = ? AND followee_id IN (?) — один запит на всю сторінку.
Оптимізований запит для перевірки isFollowedByMe
SELECT followee_id FROM follows WHERE follower_id = ? AND followee_id IN (?, ?, ?); На iOS використовуємо UITableViewDiffableDataSource з prefetching за 3 клітинки до кінця. На Android — LazyColumn з Paging 3 і RemoteMediator. Для SwiftUI — List з PrefetchingDataSource, для Jetpack Compose — LazyColumn з PagingData.
Повідомлення при підписці
При новій підписці надсилається push-повідомлення: «Іван підписався на вас» (FCM/APNs). Deeplink веде на профіль того, хто підписався. Батчинг: якщо за хвилину підписалося 5 осіб — одне повідомлення «5 нових підписників».
| Тип повідомлення | Канал | Батчинг |
|---|---|---|
| Нова підписка | Push (FCM/APNs) | До 5 подій за хвилину |
| Прийняття заявки | Push | Немає |
Налаштування push-повідомлень вимагає коректної роботи з сертифікатами APNs та ключами FCM. Ми готуємо provisioning profile для iOS та google-services.json для Android.
Рекомендації «Кого підписатися»
Проста евристика — друзі друзів. SQL:
SELECT DISTINCT f2.followee_id FROM follows f1 JOIN follows f2 ON f1.followee_id = f2.follower_id WHERE f1.follower_id = :me AND f2.followee_id != :me AND NOT EXISTS (SELECT 1 FROM follows WHERE follower_id = :me AND followee_id = f2.followee_id) LIMIT 20; Для великих графів попередній розрахунок через воркер в Redis скорочує час відповіді до 5 мс.
Як ми розробляємо модуль підписок: етапи роботи
| Етап | Тривалість | Результат |
|---|---|---|
| Аналіз | 1 день | Визначення навантаження та вимог |
| Проектування | 1 день | Схема БД, індекси, кеш |
| Реалізація API | 1–2 дні | Ендпоїнти follow/unfollow, список, рекомендації |
| Інтеграція клієнта | 1–2 дні | iOS (Swift/SwiftUI), Android (Kotlin/Jetpack Compose) |
| Тестування | 1 день | Навантажувальне тестування до 1 млн підписників |
| Деплой та моніторинг | 0.5 дня | Інструкція з розгортання |
Що входить в роботу
- Схема бази даних з індексами та денормалізацією
- Серверні API-ендпоїнти (REST або GraphQL)
- Клієнтський код на iOS (Swift/SwiftUI) та Android (Kotlin/Jetpack Compose)
- Push-повідомлення з батчингом
- Документація по API та інтеграції
- Інструкція з деплою та моніторингу
Терміни
Базова система (follow/unfollow, лічильники, список) — 1‑2 дні. Із закритими акаунтами, повідомленнями та рекомендаціями — 3‑5 днів. Вартість розраховується індивідуально. Отримайте консультацію — оцінимо ваш проект за один день. Зв'яжіться з нами, щоб обговорити деталі та замовити розробку. Ми також допомагаємо з публікацією в App Store та Google Play, враховуючи вимоги App Store Review Guidelines та Google Play Console.







