A dating app backend service using golang, utilize Echo for routing, GORM for ORM, PostgreSQL as the database, and Redis for caching.
Repository URL : https://github.com/ghaniswara/dating-app
- /internal/config : Configuration Loader for the Server
- /internal/routes : Routes for the Server
- /v1/auth : Authentication Routes
- /v1/match : Match Routes
- /internal/usecase
- /auth : Authentication Usecases
- /match : Match Usecases
- /internal/middleware : Middleware for the Server
- /internal/repository : Repositories for the Server
- /internal/entity : which consist of following entities
- repository
- request
- response
- /pkg/http_util : HTTP Utility for the Server
- /pkg/jwt : JWT Utility for the Server
- /pkg/path : Utility for searching path used by the Config Loader & Test Helper
- /test/auth : Authentication Test
- /test/helper : Test Helper
- /test/match : Match Test
- Clone the repository
- Install the dependencies using
go mod tidy - Setup the environment variables using
cp .env.example .env - Install golang-migrate by following the instruction on the website
- Run migration using golang-migrate
`$ migrate -source ./migrations/ -database postgres://localhost:<YOUR-PORT>/database up 2` - Run the server using
go run . dev
For the test we're using Ory/Dockertest which allows us to run integration test with dockerized database
Simply run go test ./test/* which will run all the test cases
If an error eccountered due to port collision, you need to run the test separately for the Auth and Match Cases this is due to TestMain in both test cases is running on the same port
-
Auth Test
`$ go test ./test/auth` -
Match Test
`$ go test ./test/match`⚠️ Will currently fail for the Match Test, due toTestNoSameProfileis implemented without parallel test in mind, which will cause data to be inconsistent,
- Endpoint to register a new user
- Endpoint to login
- Endpoint to get dating profiles
- It should accept a list of excluded profiles ID
- It should not return a same profile which has been swiped by the user on that day
- It should not return a profile which has been matched with the user
- Endpoint to swipe dating profile
- It should accept a profile ID and action (like or pass)
- It should return a outcome of the swipe
- If user swipe like more than 10 times, user shouldn't be able to swipe like anymore
- User can likes and pass other users
- User can likes up to 10 times for free, if user want to increase the limit, user need to buy the premium subscription
- User will only see dating profile that they haven't swiped yet
- When like a profile which like back, user will notified immediately
- When user pass a profile which likes the user back, user will notified immediately that he missed the chance to match with that profile
- When a user swipe like more than 10 times, user shouldn't be able to perform any like action but can still pass other users
-
Golang
The ease of use of goroutine which allows for us to utilize the distributed processing for future development, which is critical for the scalability of the service, in distributed system which needs to handle a large number of requests per seconds.
Golang is a simple language which similar to C, which is developer friendly and easy to understand.
-
Echo Router
Based on benchmark from from techempower JSON Serialization benchmark ehco is one of the fastest compared to chi and golang stardard library, a hight throughput is critical for an application such as tinder, which needs to handle a lot of RPS
Echo is also compatible with the standard go net/http library, which gives flexibility for developers
https://www.techempower.com/benchmarks/#hw=ph&test=json§ion=data-r22&l=zijocf-cn3
-
PostgreSQL
PostgreSQL offer extensive support for SQL and it's a battle-tested database which is used by many big companies, which ensures that the database part of the service is reliable and scalable.
Postgres Extension such as - pg_partman for partitioning the table for better performance and easier backup. - pg_mq for message queueing which can be used to handle the event when user liked or passed a profile. in the early phase of the development - pg_cron for scheduling the task to run periodically.
furthermore supabase involvement in the Package manager database.dev which will alow tons of community support for the database ecosystem.
-
Redis
Redis is used to cache the most frequently accessed data from the database, which will improve the performance of the service.
erDiagram
USERS {
BIGSERIAL id PK
VARCHAR name
VARCHAR email
VARCHAR username
VARCHAR password
BOOLEAN is_premium
TIMESTAMP created_at
TIMESTAMP updated_at
}
SWIPE_TRANSACTIONS {
SERIAL id PK
BIGINT user_id FK
BIGINT to_id FK
DATE date
SMALLINT action
TIMESTAMP timestamp
BOOLEAN is_matched
}
USERS ||--o{ SWIPE_TRANSACTIONS : "makes"
USERS ||--o{ SWIPE_TRANSACTIONS : "receives"
- Create Swipe Transaction
sequenceDiagram
participant User
participant MatchRepo
participant DB
participant Redis
User->>MatchRepo: CreateSwipe(ctx, userID, likedToUserID, action)
MatchRepo->>DB: Check if liked profile exists (likedToUserID)
DB-->>MatchRepo: Return likedProfileRes
alt Profile Not Found
MatchRepo-->>User: OutcomeNotFound
else Profile Found
alt Action is Like or SuperLike
MatchRepo->>Redis: Increment liked count cache
MatchRepo->>Redis: Append liked profiles cache
MatchRepo->>DB: Check if both profiles like each other
DB-->>MatchRepo: Return resPair
alt Pair Found
MatchRepo->>DB: Create SwipeTransaction
DB-->>MatchRepo: Return res
MatchRepo->>DB: Update isMatched for pair
DB-->>MatchRepo: Return update result
MatchRepo->>Redis: Append match profiles cache
MatchRepo-->>User: OutcomeMatch
else Pair Not Found
MatchRepo->>DB: Create SwipeTransaction
DB-->>MatchRepo: Return res
MatchRepo-->>User: OutcomeNoLike
end
else Action is Pass
MatchRepo-->>User: OutcomeMissed
end
end
- Get Matched Profiles IDs
sequenceDiagram
participant User
participant MatchRepo
participant DB
participant Redis
User->>MatchRepo: GetMatchedProfilesIDs(ctx, userID)
MatchRepo->>Redis: Check for matched profiles (profilesKey)
Redis-->>MatchRepo: Return profiles or redis.Nil
alt Profiles Found
MatchRepo-->>User: Return profiles
else No Profiles Found
MatchRepo->>DB: Query SwipeTransaction for matched profiles
DB-->>MatchRepo: Return profiles
MatchRepo->>Redis: Add matched profiles to Redis
Redis-->>MatchRepo: Confirm addition
MatchRepo->>Redis: Set expiration for profilesKey
MatchRepo-->>User: Return profiles
end
- GetTodayLikedProfilesIDs
sequenceDiagram
participant MatchRepo as matchRepo
participant Redis
participant Database
matchRepo->>Redis: SMembers(profilesKey)
alt Profiles found in Redis
Redis-->>matchRepo: return profiles
matchRepo-->>User: return profiles
else Profiles not found (redis.Nil)
Redis-->>matchRepo: return redis.Nil
matchRepo->>Database: getLikedProfilesIDs(userID, now)
alt Database query fails
Database-->>matchRepo: return error
matchRepo-->>User: return error
else Database query succeeds
Database-->>matchRepo: return profiles
matchRepo->>Redis: SAdd(profilesKey, profiles)
matchRepo->>Redis: Expire(profilesKey, TTL)
matchRepo-->>User: return profiles
end
end
- GetTodayLikesCount
sequenceDiagram
participant MatchRepo as m
participant Redis
participant Database
m->>Redis: Get(countKey)
alt Count found in Redis
Redis-->>m: return count
m-->>User: return count
else Count not found (redis.Nil)
Redis-->>m: return redis.Nil
m->>Database: getLikesCount(userID, now)
alt Database query fails
Database-->>m: return error
m-->>User: return error
else Database query succeeds
Database-->>m: return count
m->>Redis: Set(countKey, count, TTL)
m-->>User: return count
end
end
- Get Dating Profiles
sequenceDiagram
participant User
participant MatchUseCase
participant MatchRepo
User->>MatchUseCase: GetDatingProfiles(ctx, userID, excludeProfiles, limit)
MatchUseCase->>MatchRepo: GetTodayLikedProfilesIDs(ctx, userID)
MatchRepo-->>MatchUseCase: Return likedProfiles or error
alt Error Occurred
MatchUseCase-->>User: Return error
else No Error
MatchUseCase->>MatchRepo: GetMatchedProfilesIDs(ctx, userID)
MatchRepo-->>MatchUseCase: Return matchedProfiles or error
alt Error Occurred
MatchUseCase-->>User: Return error
else No Error
MatchUseCase->>MatchUseCase: Append likedProfiles and matchedProfiles to excludeProfiles
MatchUseCase->>MatchRepo: GetDatingProfilesIDs(ctx, userID, excludeProfiles, limit)
MatchRepo-->>MatchUseCase: Return profiles or error
alt Error Occurred
MatchUseCase-->>User: Return error
else No Error
MatchUseCase-->>User: Return profiles
end
end
end
- Swipe Dating Profile
sequenceDiagram
participant User
participant MatchUseCase
participant MatchRepo
participant UserRepo
User->>MatchUseCase: SwipeDatingProfile(ctx, userID, likedToUserID, action)
MatchUseCase->>MatchRepo: GetTodayLikesCount(ctx, userID)
MatchRepo-->>MatchUseCase: Return likesCount or error
alt Error Occurred
MatchUseCase-->>User: Return error
else No Error
MatchUseCase->>UserRepo: GetUserByID(ctx, userID)
UserRepo-->>MatchUseCase: Return user or error
alt Error Occurred
MatchUseCase-->>User: Return error
else No Error
alt Likes Limit Reached
MatchUseCase-->>User: OutcomeLimitReached
else Likes Limit Not Reached
MatchUseCase->>MatchRepo: CreateSwipe(ctx, userID, likedToUserID, action)
MatchRepo-->>MatchUseCase: Return Outcome or error
alt Error Occurred
MatchUseCase-->>User: Return error
else No Error
alt Outcome is Match
MatchUseCase-->>User: OutcomeMatch
else Outcome is Not Match
MatchUseCase-->>User: Return Outcome
end
end
end
end
end
- Signup User
sequenceDiagram
participant User
participant AuthUseCase as authUseCase
participant UserRepo as userRepo
User->>AuthUseCase: SignupUser(authData)
AuthUseCase->>AuthUseCase: Generate hashedPassword
alt Password generation fails
AuthUseCase-->>User: return error
else Password generation succeeds
AuthUseCase->>UserRepo: CreateUser(user)
UserRepo-->>AuthUseCase: return created user
AuthUseCase-->>User: return created user
end
- Signin User
sequenceDiagram
participant User
participant AuthUseCase as authUseCase
participant UserRepo as userRepo
participant JWT
User->>AuthUseCase: SignIn(email, username, password)
AuthUseCase->>UserRepo: GetUserByUnameOrEmail(email, username)
alt User not found
UserRepo-->>AuthUseCase: return error
AuthUseCase-->>User: return error
else User found
UserRepo-->>AuthUseCase: return user
AuthUseCase->>AuthUseCase: Compare password
alt Password mismatch
AuthUseCase-->>User: return error
else Password match
AuthUseCase->>JWT: CreateToken(user.ID, user.Username)
alt Token creation fails
JWT-->>AuthUseCase: return error
AuthUseCase-->>User: return error
else Token creation succeeds
JWT-->>AuthUseCase: return token
AuthUseCase-->>User: return token
end
end
end
- Should Success on valid sign up form
- Should Return error on invalid sign up form
- Email is empty
- Email is invalid
- Username is empty
- Password is empty
- Should Success on valid sign in form
- Should Return error on invalid sign in form
- Email/Username is empty
- Email is invalid
- Password is empty
- Should Success on get dating profiles
- Should not return same profile which has been swiped by the user on that day
- Should not return a profile which has been matched with the user
- Should Success on swipe dating profile
- Return OutcomeMatch when user swipe like and the profile likes the user back
- Return OutcomeNoLike when user swipe like and the profile doesn't like the user back
- Return OutcomeMissed when user swipe pass
- Should Return OutcomeLimitReached when non premium user swipe like more than 10 times
- Should not be able to like nonexistent user