Розробка системи підписок на користувачів в мобільній програмі
Підписки — основа соціального графу програми. Технічно це таблиця follows (follower_id, followee_id, created_at) з UNIQUE-обмеженням. Але деталі в 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 потрібен для швидкого підсумовування підписчиків: SELECT COUNT(*) FROM follows WHERE followee_id = ?. Без нього при 1M записях — full scan.
Денормалізація: users.followers_count та users.following_count — оновлюються триггером або чергою. Лічильник в профілі береться з денормалізованого поля, не з COUNT.
Взаємна підписка (mutual follow): SELECT 1 FROM follows WHERE follower_id = $1 AND followee_id = $2 — перевірте обидва напрями. Можна кешувати в Redis: SISMEMBER user:{id}:following {target_id}.
Кнопка Follow/Unfollow
Оптимістичне оновлення обов'язково — кнопка переключається миттєво:
// 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)
}
На Compose — followState: Boolean у ViewModel, змінюється до запиту, откатується при помилці.
Три стани кнопки: «Підписатися», «Підписан», «Відписатися» (останнє показується при лонг-тапі або hover). Не показуйте кнопку «Відписатися» як основний текст — користувачі приймають за підтвердження підписки.
Закриті аккаунти (Private accounts)
Якщо програма підтримує закриті профілі: follow_request замість виступу підписки. Нова таблиця follow_requests (requester_id, target_id, status, created_at). Цільовий користувач бачить вхідні заявки, приймає/відхиляє. При прийнятті — запис переходить у follows. Push-сповіщення при новій заявці та при прийнятті.
Список підписчиків/підписок
Пагінація cursor-based за created_at DESC або по follower_id (для стабільного порядку). Для кожного користувача у списку показуйте isFollowedByMe — потребує JOIN або batch-перевірки. Batch-варіант: SELECT followee_id FROM follows WHERE follower_id = ? AND followee_id IN (?) — один запит для всієї видимої сторінки.
На iOS — UITableView з UITableViewDiffableDataSource, кожна ячейка містить аватар, ім'я, кнопку Follow зі станом. Prefetch: UITableViewDataSourcePrefetching запитує наступну сторінку за 3 ячейки до кінця.
На Android — LazyColumn з Paging 3, RemoteMediator для мережі + локального кешу.
Сповіщення при підписці
При новій підписці — push-сповіщення автору профілю: «Іван підписався на вас». FCM/APNs через бекенд. Deeplink — на профіль підписавшегося. Батчинг: якщо за хвилину підписалось 5 осіб — одне сповіщення «5 нових підписчиків», не п'ять окремих.
Рекомендації «Кого підписатися»
Проста евристика: користувачі, на яких підписані мої підписки, але на яких не підписаний я («friends of friends»). 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;
Для великих графів — передрахунок через worker, результат у Redis.
Часові рамки
Базова система (follow/unfollow, лічильники, список) — 1-2 дні. З закритими аккаунтами, сповіщеннями, рекомендаціями — 3-5 днів. Вартість розраховується індивідуально.







