Engineering demonstration
A matrimony platform, built service by service
Our own product, built from scratch: nine Spring Boot applications and a mobile app, kept in step by one written API contract and locked down with certificates.
What this is. We built this ourselves, for ourselves. It is not a client project, and we do not quote usage or performance numbers.
The problem
A match only counts when both people say yes. So every suggestion has to work from both sides, give newer profiles a fair chance, and respect preferences like community, location and lifestyle.
Behind the screen, the services must trust one another, the apps must stay in step with the backend, and we must hear about a problem before our members do.
What is in it
Signing up and signing in
Email and password, Google sign-in and Apple sign-in. Every account must verify both its email and its phone number with one-time codes, and no session starts until both are confirmed.
Keeping accounts safe
Passwords are hashed, tokens are signed and can be revoked, and sign-in endpoints are rate limited. Changing a password or a phone number needs a fresh code, and for a new number the code goes to the new number.
Leaving, and coming back
A member can deactivate, which hides the profile everywhere, or delete the account, which starts a 15-day grace period before everything is purged. Events carry each change to the other services, so nothing is left behind.
Profiles
A five-step setup, then depth: about me, beliefs, hobbies, curated tags and structured personal details. Partner preferences are always valid, and the lists people choose from, such as religion and education, are cached for speed.
Privacy and photos
Members decide who sees what, including showing photos only to their connections. Photos are stored in S3-compatible storage, with a primary photo, deletion, a completion percentage, and an opt-in list of who viewed your profile.
Matching
Every pair is scored from both sides and combined, across community, age, height, marital status, diet, smoking, drinking, education, income, location, language and tags. A compatibility check works against any profile.
The feed and daily picks
A match feed with modes, and daily picks prepared overnight. If a strict search finds too few people, the criteria loosen step by step and the response says which ones were relaxed. Like, super-like and skip are remembered, and skipped profiles can come back later.
Connections
Send, accept, reject or cancel a request, and see your inbox, sent and active connections. Requests also appear automatically from interest events. Real-time chat, with read receipts and presence, is being added on top.
Emails that arrive
Verification and password-reset emails go out through the Gmail API, triggered by messages from RabbitMQ, so signing up never waits on an email server.
One front door for the apps
A single backend-for-frontend sits between the apps and the services. Web gets a cookie session, mobile gets tokens, and every response has a type that matches the contract.
The mobile app
A React Native (Expo) app with sign-in, profile setup, a swipeable match feed and connection requests. It keeps a cached copy of your data, stores tokens securely, and uses native iOS glass effects with a fallback for older phones, including a small SwiftUI action bar.
Engineering decisions
- 01
Look at every match from both sides
We score each pair from each person's point of view and combine the two with a weighted harmonic mean. That way a match that suits only one person cannot rank high. The scoring is split into small, separate scorers, one for each dimension.
- 02
Fairness is part of the ranking
Ranking runs in separate stages for exposure, recent activity and verification, and each page is shaped for freshness, variety and a little exploration. Candidate pools mix active, random and new profiles, so a list sorted purely by score does not leave newcomers unseen. We also track how stable the ranking is.
- 03
Prepare the work ahead of time
A nightly job scores a deep pool and saves the daily picks. Reads come from a Redis cache, are checked again when served, and carry on working, a little more simply, if Redis is down.
- 04
Write the contract first
Every service starts from an OpenAPI file and generates its controllers and types from it. The front-door service reuses the owning service's response shape instead of copying it. In CI, the specs are linted and a breaking change fails the build.
- 05
Services prove who they are
Every call between services uses mutual TLS, with certificates from our own authority, and each service keeps a short list of who may call it. Only the reverse proxy faces the internet.
- 06
Send events only after the save
Events go to RabbitMQ only after the database commit, so a rollback can never leave a ghost message behind. A correlation id travels with each request, over HTTP and over messages, so one user action can be followed end to end.
- 07
Keep the layers clean
Generated API types stay at the edge, database entities stay inside persistence, and the service layer talks only in validated domain objects. In the user service, automated architecture tests fail the build if a layer reaches where it should not.
- 08
Test it the way it runs
Tests are written as given, when, then. Integration tests run against a real PostgreSQL that is kept separate from the development one, so a cleanup can never touch real data.
- 09
See problems early
Every service reports metrics to Prometheus, with Grafana dashboards for each API and one for the sign-up funnel. Logs go to Loki carrying the correlation id, so you can follow a graph down to the exact request.
- 10
Rebuild from nothing, on paper
Settings live in a config server with separate development and live folders. Secrets come from a secret manager, not from git, and a written runbook describes how to rebuild the whole thing from scratch.
Go deeper
Technology
- Java
- Spring Boot
- Spring Cloud Gateway
- Eureka
- Spring Cloud Config
- PostgreSQL
- MongoDB
- Redis
- RabbitMQ
- S3-compatible storage
- Liquibase
- OpenAPI
- MapStruct
- React Native
- Expo
- TypeScript
- TanStack Query
- Zustand
- SwiftUI
- Docker
- Caddy
- GitHub Actions
- Spectral and oasdiff
- GCP Secret Manager
- Prometheus
- Grafana
- Loki
Facing a similar problem?
Tell us what you are building or trying to fix. We will reply with an honest view.