A Rails API for tracking users' sleep patterns and viewing weekly sleep rankings among followed users.
- Features
- Tech Stack
- Architecture
- Database Schema
- Setup
- Assumptions
- API Documentation
- Testing
- Background Jobs
- Scalability & Performance
- Contributing
Users can log when they sleep/wake and compare weekly sleep stats with friends.
- Sleep Tracking: Record when users go to bed and wake up using a single
clock_inendpoint - Social Features: Follow and unfollow other users to build a sleep tracking community
- Weekly Rankings: View weekly sleep records of followed users, sorted by total sleep duration
- Scalable Design: Built with caching, database indexing, background jobs, and clean architecture patterns
- Timezone Support: Handle sleep records across different timezones
- Pagination: Efficient data retrieval with built-in pagination support
- Ruby: 3.0.0 (see
.ruby-version) - Rails: 7.1.0 (API mode)
- PostgreSQL: Primary database with support for complex queries and excellent scalability
- Redis: Caching layer for leaderboards and sleep records
- Sidekiq: Background job processing with cron scheduling
- RSpec: Comprehensive testing framework
- Docker: Containerization support
- Data & Validation:
dry-validation,dry-monads,dry-structfor robust data handling - Serialization:
albafor fast JSON serialization - Architecture:
boxennfor domain-driven design patterns - Database:
scenicfor database views,pgfor PostgreSQL - Background Jobs:
sidekiq,sidekiq-cronfor scheduled tasks
This application follows Domain-Driven Design (DDD) principles with clean architecture:
app/
βββ api/
β βββ contracts/ # Request validation
β βββ serializers/ # Response serializers
βββ controllers/ # API controllers
βββ domains/ # Domain logic (DDD)
β βββ master_data/ # User management domain
β βββ relationship/ # Follow/unfollow domain
β βββ track_management/ # Sleep tracking domain
βββ models/
βββ services/ # Application services
β βββ cache/ # Caching services
β βββ refresher/ # Data refresh services
β βββ use_cases/ # Business use cases
βββ workers/ # Background job workers
The application implements Domain-Driven Design (DDD) with three distinct bounded contexts:
Responsibility: Core user management and identity
- Entities:
User: Basic user entity with id and name
- Repositories:
User: User data access with existence checks
- Key Features:
- User identity management
- User existence validation
- Simple user profile data
Responsibility: Social connections and follow relationships
- Entities:
Follow: Follow relationship with validation (users cannot follow themselves)
- Repositories:
Follow: Thread-safe follow operations with advisory locks
- Key Features:
- Follow/unfollow operations
- Deadlock prevention through consistent lock ordering
- Self-follow validation
- User relationship queries
Responsibility: Sleep tracking and analytics
- Entities:
SleepRecord: Sleep session with timezone support and duration calculation
- Repositories:
SleepRecord: Complex sleep record operations with advisory locksFollowingsWeeklySleepRanking: Weekly ranking calculations
- Key Features:
- Clock-in/clock-out sleep tracking
- Timezone-aware sleep records
- Incomplete record handling (sleep without wake)
- Weekly ranking aggregations
- Duration calculations
The application uses a simple but effective database schema with three main entities:
erDiagram
USERS {
integer id PK
string name
}
FOLLOWS {
integer id PK
integer follower_id FK
integer followed_id FK
}
SLEEP_RECORDS {
integer id PK
integer user_id FK
datetime sleep_at
datetime wake_at
integer duration
string sleep_timezone
string wake_timezone
}
USERS ||--o{ SLEEP_RECORDS : "has many"
USERS ||--o{ FOLLOWS : "follower"
USERS ||--o{ FOLLOWS : "followed"
- Users: Core user entity with basic profile information
- Follows: Many-to-many relationship table enabling users to follow each other
- Sleep Records: Tracks individual sleep sessions with timezone support and duration calculation
- Ruby 3.0.0
- PostgreSQL 12+
- Redis 6+
# Clone repository
git clone https://github.com/Rae-Lee/sleep_tracker_rails.git
cd sleep_tracker_rails
# Install dependencies
bundle install
# Setup database
rails db:create db:migrate db:seed
# Start Redis (if not running)
redis-server
# Start Sidekiq (for background jobs)
bundle exec sidekiq
# Run server
rails serverThe API will be available at http://localhost:3000.
# Build and run with Docker
docker build -t sleep-tracker-api .
docker run -p 3000:3000 sleep-tracker-apiAPI Design: A single clock_in API handles both clock-in and clock-out actions.
Call Sequence:
- First call β creates a record with
sleep_at(bedtime). - Second call β updates the same record with
wake_at(wake-up time).
Behavior:
- No unfinished record β create a new
sleep_atentry. - Unfinished record exists β update it with
wake_at. - Response: Always returns all user records (including unfinished one).
Cross-day: Supported (e.g., sleep at 23:00 β wake at 07:00).
Cross-week: If a sleep record spans across weeks, it is still included when retrieving the ranking of all followed users for the previous week. The record is assigned to the week based on its sleep_at value.
Example: Sleep on Sunday 23:00, wake on Monday 07:00 β the record is counted as part of Sundayβs week.
- All timestamps are stored in UTC.
- API responses use ISO8601 format.
- The system supports different timezones for sleep_at and wake_at (e.g., in travel scenarios).
follows(follower_id, followed_id)must be unique; duplicate follows are not allowed.
Base URL: http://localhost:3000/api/v1
All endpoints require an identity_id parameter to identify the user making the request.
Record sleep or wake time for a user.
Endpoint
POST /api/v1/sleep_records/clock_inRequest Body
{
"identity_id": 1,
"sleep_at": "2024-01-15T22:30:00Z",
"wake_at": "2024-01-16T07:00:00Z",
"sleep_timezone": "Asia/Taipei",
"wake_timezone": "Asia/Taipei"
}Parameters
identity_id(required): User IDsleep_at(required): Sleep timestamp in ISO 8601 formatwake_at(optional): Wake timestamp in ISO 8601 formatsleep_timezone(optional): Timezone for sleep timewake_timezone(optional): Timezone for wake time
Response
{
"user_id": 1,
"data": [
{
"sleep_at": "2025-08-23T22:30:00+00:00",
"wake_at": "2025-08-24T07:00:00+00:00",
"sleep_timezone": "Asia/Taipei",
"wake_timezone": "Asia/Taipei",
"sleep_at_in_timezone": "2025-08-24T06:30:00+08:00",
"wake_at_in_timezone": "2025-08-24T15:00:00+08:00",
"duration": 30600,
"status": "completed"
}
],
"pagination": {
"page": 1,
"per_page": 10,
"total_count": 1,
"total_pages": 1
}
}Endpoint
POST /api/v1/followsRequest Body
{
"identity_id": 1,
"followed_id": 2
}Parameters
identity_id(required): ID of the user making the follow requestfollowed_id(required): ID of the user to follow
Response
{
"user_id": 1,
"data": {
"followed_id": 5,
"followed_name": "Emma Brown",
},
"created_at": "2024-01-15T10:00:00Z"
}Endpoint
DELETE /api/v1/follows/:idParameters
identity_id(required): ID of the user making the unfollow requestid(URL parameter): Follow relationship ID
Response
{
"message": "Successfully unfollowed user"
}Retrieve weekly sleep rankings for all followed users.
Endpoint
GET /api/v1/followings/sleep_rankingQuery Parameters
identity_id(required): User ID requesting the rankingspage(optional): Page number (default: 1)per_page(optional): Records per page (max: 50, default: 20)
Response
{
"user_id": 1,
"week_start": "2024-01-15",
"week_end": "2024-01-21",
"data": [
{
"followed_id" : 2,
"followed_name": "john_doe",
"sleep_at": "2024-01-15T22:30:00+00:00",
"wake_at": "2024-01-16T07:00:00+00:00",
"sleep_timezone": "Asia/Taipei",
"wake_timezone": "Asia/Taipei",
"sleep_at_in_timezone": "2024-01-16T06:30:00+08:00",
"wake_at_in_timezone": "2024-01-16T15:00:00+08:00",
"duration": 30600,
}
],
"pagination": {
"page": 1,
"per_page": 20,
"total_count": 5,
"total_pages": 1
}
}All endpoints return consistent error responses:
{
"error": "Validation failed",
"errors": {
"followed_id": ["must be a positive integer"]
}
}Common HTTP status codes:
200: Success400: Bad Request404: Not Found422: Unprocessable Entity500: Internal Server Error
Run the test suite:
# Run all tests
bundle exec rspec
# Run specific test file
bundle exec rspec spec/requests/api/v1/sleep_records/clock_ins_spec.rb
# Run tests with coverage
COVERAGE=true bundle exec rspec- Request specs: API endpoint testing
- Service specs: Business logic testing
- Factory specs: Test data generation
- Worker specs: Background job testing
The application uses Sidekiq for background processing:
Automatically refreshes weekly sleep rankings using database views.
# Scheduled via sidekiq-cron
WeeklySleepRankingRefreshWorker.perform_async# Start Sidekiq
bundle exec sidekiq
# Start with specific configuration
bundle exec sidekiq -C config/schedule.ymlThe system is designed to efficiently handle a growing user base, managing high data volumes and concurrent requests through multiple optimization strategies:
Strategic Index Placement:
-- Composite index for sleep record queries
CREATE INDEX index_track_management_sleep_records_on_user_id_and_sleep_at
ON track_management_sleep_records (user_id, sleep_at);
-- Unique constraint with index for follow relationships
CREATE UNIQUE INDEX idx_on_follower_id_followed_id
ON relationship_follow_records (follower_id, followed_id);
-- Materialized view index for fast ranking queries
CREATE UNIQUE INDEX index_followings_weekly_sleep_rankings_unique
ON track_management_followings_weekly_sleep_rankings (followed_id, sleep_at_utc);Benefits:
- Fast user sleep record lookups by
user_idandsleep_at - Efficient follow relationship queries with unique constraint enforcement
- Optimized weekly ranking queries through materialized view indexing
Pre-calculated Duration Storage:
- Sleep duration calculated and stored as integer (seconds) in database
- Eliminates real-time calculation overhead during queries
- Enables efficient sorting and aggregation operations
# Duration calculated once during record creation/update
duration = (wake_at.utc - sleep_at.utc).to_i
record.update!(duration: duration)Eager Loading Strategy:
# Follow repository with user data preloading
def with_users
source_wrapper.source.includes(:followed)
end
# Materialized view eliminates N+1 for ranking queries
# Single query returns all user data with sleep records joinedImplementation:
- Repository pattern with explicit
includes()for associations - Materialized views pre-join related data
- Domain entities encapsulate data access patterns
Consistent Pagination Across All Endpoints:
# Built-in pagination with configurable limits
def find_by_user_id(user_id:, page: 1, per_page: 10)
records = source_wrapper.source
.where(user_id: user_id)
.limit(per_page)
.offset((page - 1) * per_page)
end
# Validation prevents excessive page sizes
rule(:per_page) do
key.failure('cannot exceed 50 records per page') if value && value > 50
endFeatures:
- Maximum page size limits (50 records) prevent resource exhaustion
- Offset-based pagination for consistent results
- Total count tracking for client-side pagination controls
Advisory Locks for Concurrency Control:
# Prevent race conditions in sleep record operations
def save(primary_keys, attributes)
user_id = primary_keys[:user_id]
lock_key = "sleep_record_user_#{user_id}"
with_advisory_lock(lock_key) do
# Thread-safe sleep record creation/update logic
end
end
# Deadlock prevention in follow operations
def generate_lock_key(primary_keys)
ids = [follower_id, followed_id].sort # Consistent ordering
"follow_relationship_#{ids[0]}_#{ids[1]}"
endBenefits:
- Advisory locks prevent race conditions without table-level locking
- Consistent lock ordering prevents deadlocks
- User-specific locking allows concurrent operations for different users
Redis-Based Caching with Smart Invalidation:
class Cache::SleepRecordCacheService
CACHE_EXPIRY = 1.hour
PAGE_SIZE = 10
# User-specific cache keys
def cache_key
"sleep_records:user:#{user_id}:page:#{page}"
end
endclass Cache::FollowingsSleepRecordsCacheService
CACHE_EXPIRY = 8.days
HEAVY_USER_THRESHOLD = 20
# Cache only for heavy users (20+ queries)
def should_cache?
is_heavy_user? && !cache_exists? && page == 1
end
# Week-based cache keys for rankings
def cache_key
week_key = last_week_key
"followings_sleep_records:user:#{user_id}:#{week_key}:page:#{page}"
end
endCache Features:
- Selective caching: Only cache for heavy users (20+ queries per week)
- Automatic invalidation: Cache cleared on data updates
- Week-based keys: Separate cache for different time periods
- Query counting: Track user activity to optimize caching decisions
Sidekiq-Based Async Processing:
class WeeklySleepRankingRefreshWorker
include Sidekiq::Worker
sidekiq_options queue: :default, retry: 3
def perform
# Refresh materialized view
# Invalidate all caches
# Pre-warm cache for heavy users
end
end# config/schedule.yml
weekly_ranking_refresh:
cron: '0 1 * * 1' # Every Monday at 1 AM
class: WeeklySleepRankingRefreshWorkerBenefits:
- Non-blocking operations: Heavy computations moved to background
- Automatic retries: Failed jobs retry up to 3 times
- Scheduled maintenance: Weekly data refresh during low-traffic hours
- Cache pre-warming: Proactively cache data for heavy users
Pre-computed Weekly Rankings:
CREATE MATERIALIZED VIEW track_management_followings_weekly_sleep_rankings AS
SELECT
sr.user_id AS followed_id,
u.name AS followed_name,
sr.sleep_at AS sleep_at_utc,
sr.wake_at AS wake_at_utc,
sr.sleep_at AT TIME ZONE sr.sleep_timezone AS sleep_at_local,
sr.wake_at AT TIME ZONE sr.wake_timezone AS wake_at_local,
sr.sleep_timezone,
sr.wake_timezone,
sr.duration
FROM track_management_sleep_records sr
JOIN master_data_users u ON u.id = sr.user_id
WHERE
(sr.sleep_at AT TIME ZONE sr.sleep_timezone) >= date_trunc('week', NOW() AT TIME ZONE sr.sleep_timezone) - INTERVAL '1 week'
AND (sr.sleep_at AT TIME ZONE sr.sleep_timezone) < date_trunc('week', NOW() AT TIME ZONE sr.sleep_timezone)
AND sr.wake_at IS NOT NULL
ORDER BY sr.duration DESC;Advantages:
- Pre-computed joins: Eliminates expensive JOIN operations at query time
- Timezone calculations: Complex timezone conversions done once during refresh
- Filtered data: Only includes relevant records (previous week, completed sleep sessions)
- Sorted results: Pre-sorted by duration for fast ranking queries
Key Metrics to Monitor:
- Cache hit/miss ratios
- Database query execution times
- Background job queue lengths
- Materialized view refresh duration
- Advisory lock wait times
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow Ruby style guide
- Write comprehensive tests for new features
- Update documentation for API changes
- Use conventional commit messages
This project is licensed under the MIT License.