How to Design a Digital Marketplace & Freelancing Platform
Building a Production-Grade Upwork/Fiverr — Matching, Payments, Trust & Scale
1. Introduction & Why Marketplaces Are Hard
The global freelance economy is projected to surpass $455 billion by 2025, with platforms like Upwork, Fiverr, Toptal, and Freelancer commanding hundreds of millions in annual revenue. These platforms are more than simple job boards — they are complex, multi-sided marketplaces that orchestrate trust, payments, communication, dispute resolution, and talent matching across millions of users spanning every country and time zone on earth. Designing such a platform from scratch is one of the most challenging system design problems because it requires solving not just technical scale problems but also economic and behavioral ones simultaneously.
At its core, a freelancing marketplace is a two-sided platform connecting supply (freelancers with skills) with demand (clients who need work done). The fundamental marketplace problem is the cold-start chicken-and-egg dilemma: freelancers won't join without clients, and clients won't join without freelancers. Upwork solved this by acquiring two predecessor platforms (Elance and oDesk) and merging their user bases. Fiverr solved it by starting with a single, constrained use case — "I will do anything for $5" — and expanding from there. Toptal solved it by creating artificial scarcity — accepting only the "top 3%" of applicants — which attracted premium clients willing to pay higher rates.
Beyond the cold-start problem, the technical challenges are immense. A marketplace must process payments across 180 countries with dozens of currencies while complying with local tax laws, labor regulations, and anti-money-laundering (AML) requirements. It must match freelancers to jobs using algorithms that consider skills, availability, budget, ratings, response time, and cultural fit. It must manage disputes where one party claims work was not delivered and the other claims payment is owed — and it must do this at a scale of millions of transactions per month with sub-second response times for search and matching.
Real-World Case Studies
| Platform | Model | Scale | Key Differentiator |
|---|---|---|---|
| Upwork | Bidding + Escrow | 18M+ freelancers, $3.8B GMV/yr | Enterprise contracts, hourly tracking, payment protection |
| Fiverr | Gig catalog | 4M+ freelancers, $700M+ rev/yr | Predefined gigs, tiered pricing, buyer-request matching |
| Toptal | Vetted talent network | 10K+ freelancers, 20% take rate | Rigorous screening (3%), dedicated matching, premium pricing |
| Freelancer.com | Contest + bidding | 70M+ users | Contest-based design work, enterprise outsourcing |
| 99designs (Vista) | Contest marketplace | 900K+ designers | Design-only, contest model, one-to-many delivery |
Upwork's architecture is particularly instructive. Their platform handles over 10 million job postings per year with hundreds of millions of search queries. Their matching algorithm considers over 50 signals including skill overlap, job history, response rate, earnings history, client preferences, and even timezone compatibility. The payment system processes billions of dollars annually using Stripe Connect, handling milestone-based escrow, hourly tracking with screenshots, multi-currency payouts, and 1099 tax documentation for US freelancers. Each of these subsystems is a significant engineering challenge on its own.
This guide provides a comprehensive, senior-level walkthrough of designing every component of a freelancing marketplace — from the data model to the matching algorithm, from payment processing to dispute resolution. We will reference real-world patterns from Upwork, Fiverr, and Toptal throughout, providing production-tested approaches rather than theoretical exercises.
2. Functional & Non-Functional Requirements
Functional Requirements
- User Profiles: Freelancers create profiles with skills, portfolio, bio, hourly rate, availability, and work history. Clients create profiles with company info, industry, and hiring preferences.
- Skill Taxonomy: A hierarchical skill graph with parent-child relationships (e.g., Programming > Web Development > React.js) and synonyms. Skills are mapped to market demand and pricing benchmarks.
- Job Posting: Clients post jobs with descriptions, required skills, budget (fixed or hourly), duration, and experience level. Jobs are categorized and tagged for search.
- Proposal & Bidding: Freelancers submit proposals with cover letters, estimated timelines, and bid amounts. Clients can also invite specific freelancers to bid.
- Search & Ranking: Multi-signal search ranking considering skill match, rating, job success score, relevance, recency, and client budget.
- Contract Management: Once a proposal is accepted, a contract is created with agreed terms, milestones, payment schedule, and end date.
- Milestone-Based Payments: Escrow system where clients fund milestones, freelancers deliver work, clients approve, and funds are released. Dispute mechanism for disagreements.
- Time Tracking: For hourly contracts, a desktop tracker captures hours worked with periodic screenshots, activity levels, and manual time entry support.
- Messaging: Real-time chat between freelancers and clients with file sharing, read receipts, and message history.
- Review & Rating: Two-sided review system where both freelancers and clients rate each other on multiple dimensions after contract completion.
- Dispute Resolution: Structured workflow for payment disputes with evidence submission, mediation, and resolution.
- Payment Processing: Stripe Connect integration for global payments, multi-currency support, escrow, and automated payouts.
- Tax Documentation: Automated 1099 generation for US freelancers, VAT handling for EU, and invoice generation.
- Identity Verification: KYC verification for payment compliance, including government ID verification and address proof.
- Skill Assessments: Platform-administered skill tests to validate freelancer capabilities.
- Portfolio Showcase: Rich portfolio items with images, links, descriptions, and client attribution.
- Talent Pools: Clients can create private talent pools, shortlist freelancers, and manage recurring hiring.
Non-Functional Requirements
| Requirement | Target | Rationale |
|---|---|---|
| Search Latency | < 200ms (p99) | Search is the primary discovery mechanism — slow search kills conversion |
| Availability | 99.95% | Marketplace downtime means lost revenue for both platform and users |
| Payment Success Rate | > 99.9% | Failed payments erode trust and lose clients |
| Data Durability | 99.999999% (8 nines) | Financial records, contracts, and payment history must never be lost |
| Message Delivery | < 500ms (p99) | Real-time messaging is core to the user experience |
| Scale | 50M users, 10M jobs/year | Growth targets for a mid-size marketplace |
| Compliance | GDPR, CCPA, SOC 2 | Regulatory requirements for global operation |
| Time Tracking Accuracy | < 1 minute drift per 8 hours | Hourly billing accuracy directly affects trust |
| Escrow Settlement | < 3 business days | Freelancers expect prompt payment after milestone approval |
| API Rate Limit | 5000 req/min per user | Protect platform from abuse while supporting integrations |
3. Capacity Estimation & Cost
User & Transaction Volume
- Total registered users: 50 million (20M freelancers, 30M clients)
- Monthly active users: 10 million
- New job postings per day: ~28,000
- Proposals submitted per day: ~280,000 (10 proposals per job average)
- Search queries per second: ~5,000 at peak
- Messages sent per day: ~10 million
- Contracts created per day: ~5,000
- Payments processed per month: ~200,000 milestones
- Total GMV (Gross Merchandise Value) per year: ~$3 billion
Storage Estimation
- User profiles: 50M × 10 KB = 500 GB
- Job postings: 10M × 5 KB = 50 GB
- Proposals: 100M × 3 KB = 300 GB
- Messages: 1B × 2 KB = 2 TB
- File attachments: ~50 TB (growing 2 TB/month)
- Transaction records: ~10 TB (with 7-year retention)
- Search indexes: ~100 GB (Elasticsearch)
- Review data: 50M reviews × 1 KB = 50 GB
- Total initial storage: ~73 TB
Cost Estimation (Monthly, AWS)
| Service | Specification | Monthly Cost |
|---|---|---|
| Application Servers (ECS) | 8 × c6g.2xlarge | $4,800 |
| PostgreSQL (RDS) | db.r6g.2xlarge, Multi-AZ, 2 replicas | $3,500 |
| Elasticsearch | 6 × m6g.xlarge (600 GB storage) | $4,200 |
| Redis Cluster | 6 × r6g.large | $2,400 |
| Kafka | 6 × kafka.m5.2xlarge | $5,040 |
| S3 (Files) | 100 TB + transfer | $2,800 |
| CloudFront CDN | 10 TB/month transfer | $850 |
| SQS / SNS | Standard messaging | $400 |
| Monitoring (Datadog) | APM + Logs + Infra | $3,000 |
| Total Infrastructure | ~$27,000/month |
4. Data Model & Storage Schema
The data model for a freelancing marketplace is one of the most complex in the SaaS domain. It must represent users with dual roles (a freelancer can also be a client), a rich skill graph, multi-stage contracts with financial transactions, temporal data (availability, time tracking), and a complete audit trail. We use PostgreSQL as the primary relational database with Elasticsearch for search and Redis for caching.
Core Entity Relationship Diagram
Key Tables
SQLCREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(200) NOT NULL,
role VARCHAR(20) NOT NULL CHECK (role IN ('freelancer','client','both')),
avatar_url TEXT,
phone VARCHAR(20),
country_code VARCHAR(2) NOT NULL,
timezone VARCHAR(50) DEFAULT 'UTC',
email_verified BOOLEAN DEFAULT FALSE,
phone_verified BOOLEAN DEFAULT FALSE,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE freelancer_profiles (
user_id UUID PRIMARY KEY REFERENCES users(id),
headline VARCHAR(200),
bio TEXT,
hourly_rate_cents INTEGER CHECK (hourly_rate_cents >= 0),
currency VARCHAR(3) DEFAULT 'USD',
availability VARCHAR(20) DEFAULT 'full_time',
years_experience INTEGER CHECK (years_experience >= 0),
english_proficiency VARCHAR(20),
job_success_score DECIMAL(5,2) DEFAULT 0,
total_earnings_cents BIGINT DEFAULT 0,
total_jobs_completed INTEGER DEFAULT 0,
avg_rating DECIMAL(3,2) DEFAULT 0,
response_time_hours DECIMAL(5,1),
last_active_at TIMESTAMPTZ,
verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE skills (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
parent_id INTEGER REFERENCES skills(id),
category VARCHAR(100),
demand_score DECIMAL(5,2) DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE freelancer_skills (
freelancer_id UUID REFERENCES freelancer_profiles(user_id),
skill_id INTEGER REFERENCES skills(id),
proficiency VARCHAR(20) DEFAULT 'intermediate',
years_used INTEGER DEFAULT 0,
endorsed_count INTEGER DEFAULT 0,
PRIMARY KEY (freelancer_id, skill_id)
);
CREATE TABLE job_postings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_id UUID REFERENCES users(id),
title VARCHAR(300) NOT NULL,
description TEXT NOT NULL,
job_type VARCHAR(20) NOT NULL CHECK (job_type IN ('fixed','hourly')),
budget_min_cents INTEGER,
budget_max_cents INTEGER,
hourly_rate_min_cents INTEGER,
hourly_rate_max_cents INTEGER,
duration_weeks INTEGER,
experience_level VARCHAR(20),
status VARCHAR(20) DEFAULT 'open',
proposal_count INTEGER DEFAULT 0,
hire_count INTEGER DEFAULT 0,
visibility VARCHAR(20) DEFAULT 'public',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ
);
CREATE TABLE contracts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_id UUID REFERENCES job_postings(id),
client_id UUID REFERENCES users(id),
freelancer_id UUID REFERENCES users(id),
title VARCHAR(300) NOT NULL,
job_type VARCHAR(20) NOT NULL,
total_amount_cents BIGINT,
currency VARCHAR(3) DEFAULT 'USD',
status VARCHAR(20) DEFAULT 'active',
start_date DATE,
end_date DATE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE milestones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
contract_id UUID REFERENCES contracts(id),
title VARCHAR(300) NOT NULL,
description TEXT,
amount_cents BIGINT NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
due_date DATE,
order_index INTEGER NOT NULL,
funded_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
approved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE escrow_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
milestone_id UUID REFERENCES milestones(id),
client_id UUID REFERENCES users(id),
freelancer_id UUID REFERENCES users(id),
amount_cents BIGINT NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
status VARCHAR(20) DEFAULT 'held',
stripe_transfer_id VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT NOW(),
released_at TIMESTAMPTZ
);
CREATE TABLE reviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
contract_id UUID REFERENCES contracts(id),
reviewer_id UUID REFERENCES users(id),
reviewee_id UUID REFERENCES users(id),
communication INTEGER CHECK (communication BETWEEN 1 AND 5),
quality INTEGER CHECK (quality BETWEEN 1 AND 5),
timeliness INTEGER CHECK (timeliness BETWEEN 1 AND 5),
expertise INTEGER CHECK (expertise BETWEEN 1 AND 5),
overall INTEGER CHECK (overall BETWEEN 1 AND 5),
comment TEXT,
is_public BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE time_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
contract_id UUID REFERENCES contracts(id),
freelancer_id UUID REFERENCES users(id),
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ,
duration_minutes INTEGER,
description TEXT,
screenshot_urls TEXT[],
activity_level INTEGER CHECK (activity_level BETWEEN 0 AND 100),
status VARCHAR(20) DEFAULT 'tracked',
approved BOOLEAN,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL,
sender_id UUID REFERENCES users(id),
content TEXT NOT NULL,
message_type VARCHAR(20) DEFAULT 'text',
attachment_urls TEXT[],
read_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE disputes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
contract_id UUID REFERENCES contracts(id),
milestone_id UUID REFERENCES milestones(id),
initiated_by UUID REFERENCES users(id),
reason VARCHAR(50) NOT NULL,
description TEXT NOT NULL,
amount_cents BIGINT NOT NULL,
status VARCHAR(20) DEFAULT 'open',
resolution TEXT,
resolved_by UUID REFERENCES users(id),
resolved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
contract_id UUID REFERENCES contracts(id),
client_id UUID REFERENCES users(id),
freelancer_id UUID REFERENCES users(id),
amount_cents BIGINT NOT NULL,
fee_cents BIGINT NOT NULL,
net_amount_cents BIGINT NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
status VARCHAR(20) DEFAULT 'pending',
invoice_number VARCHAR(50) UNIQUE NOT NULL,
issued_at TIMESTAMPTZ DEFAULT NOW(),
paid_at TIMESTAMPTZ
);
Indexing Strategy
SQLCREATE INDEX idx_freelancer_skills_skill ON freelancer_skills(skill_id);
CREATE INDEX idx_freelancer_profiles_rate ON freelancer_profiles(hourly_rate_cents);
CREATE INDEX idx_freelancer_profiles_jss ON freelancer_profiles(job_success_score DESC);
CREATE INDEX idx_job_postings_status ON job_postings(status, created_at DESC);
CREATE INDEX idx_job_postings_client ON job_postings(client_id, status);
CREATE INDEX idx_proposals_job ON proposals(job_id, created_at);
CREATE INDEX idx_proposals_freelancer ON proposals(freelancer_id, status);
CREATE INDEX idx_contracts_status ON contracts(status, updated_at);
CREATE INDEX idx_milestones_contract ON milestones(contract_id, order_index);
CREATE INDEX idx_milestones_status ON milestones(status, due_date);
CREATE INDEX idx_messages_conversation ON messages(conversation_id, created_at);
CREATE INDEX idx_time_entries_contract ON time_entries(contract_id, start_time);
CREATE INDEX idx_reviews_reviewee ON reviews(reviewee_id, created_at DESC);
CREATE INDEX idx_escrow_status ON escrow_transactions(status, created_at);
5. High-Level Architecture Overview
The platform follows a microservices architecture with clear domain boundaries. Each bounded context — user management, job postings, search, payments, messaging, reviews — is an independent service with its own database (database-per-service pattern). Services communicate through synchronous gRPC for internal calls and asynchronous Kafka events for cross-service data propagation. The API Gateway handles authentication, rate limiting, and request routing.
Service Responsibilities
| Service | Responsibility | Database | Key Tech |
|---|---|---|---|
| User Service | Registration, auth, OAuth, role management | PostgreSQL | JWT, OAuth2, bcrypt |
| Profile Service | Freelancer/client profiles, portfolio, availability | PostgreSQL | Image processing pipeline |
| Skill Service | Skill taxonomy, skill graph, demand analytics | PostgreSQL | Graph queries (recursive CTEs) |
| Job Service | Job CRUD, categorization, status lifecycle | PostgreSQL | State machine |
| Proposal Service | Proposal submission, client review, acceptance | PostgreSQL | Idempotency, rate limiting |
| Search Service | Indexing, full-text search, ranking, facets | Elasticsearch | BM25, learning-to-rank |
| Matching Service | NLP-based matching, recommendations | PostgreSQL + Redis | Embeddings, cosine similarity |
| Contract Service | Contract lifecycle, milestones, status transitions | PostgreSQL | State machine, saga pattern |
| Payment Service | Stripe Connect, charge processing, payouts | PostgreSQL | Stripe SDK, idempotency keys |
| Escrow Service | Fund holding, release, refund logic | PostgreSQL | Double-entry bookkeeping |
| Messaging Service | Real-time chat, read receipts, history | Redis + PostgreSQL | WebSockets, SignalR |
| File Service | Upload, virus scan, CDN distribution | S3 | ClamAV, pre-signed URLs |
| Review Service | Two-sided reviews, score computation | PostgreSQL | Weighted average, fraud detection |
| Dispute Service | Dispute lifecycle, evidence, resolution | PostgreSQL | Workflow engine |
| Notification Service | Email, push, in-app notifications | Redis (ephemeral) | SendGrid, Firebase, WebSocket |
Communication Patterns
Synchronous gRPC is used for internal service-to-service calls where a response is needed immediately (e.g., the Job Service calling the User Service to validate a client ID). Asynchronous Kafka events are used for data propagation where eventual consistency is acceptable (e.g., when a contract is created, an event is published that the Invoice Service and Notification Service consume independently). This separation ensures that the core transaction (creating a contract) is not coupled to downstream processing.
C#public class ContractCreatedEvent
{
public Guid ContractId { get; set; }
public Guid ClientId { get; set; }
public Guid FreelancerId { get; set; }
public string JobType { get; set; }
public long TotalAmountCents { get; set; }
public string Currency { get; set; }
public List<MilestoneDto> Milestones { get; set; }
public DateTime CreatedAt { get; set; }
}
public class ContractService : IContractService
{
private readonly AppDbContext _db;
private readonly IKafkaProducer _kafka;
public async Task<Contract> CreateContractAsync(CreateContractCommand cmd)
{
await using var transaction = await _db.Database.BeginTransactionAsync();
try
{
var contract = new Contract
{
Id = Guid.NewGuid(),
JobId = cmd.JobId,
ClientId = cmd.ClientId,
FreelancerId = cmd.FreelancerId,
Title = cmd.Title,
JobType = cmd.JobType,
TotalAmountCents = cmd.TotalAmountCents,
Currency = cmd.Currency,
Status = ContractStatus.Active,
StartDate = DateOnly.FromDateTime(DateTime.UtcNow)
};
_db.Contracts.Add(contract);
foreach (var ms in cmd.Milestones)
{
_db.Milestones.Add(new Milestone
{
Id = Guid.NewGuid(),
ContractId = contract.Id,
Title = ms.Title,
Description = ms.Description,
AmountCents = ms.AmountCents,
OrderIndex = ms.OrderIndex,
Status = MilestoneStatus.Pending
});
}
var job = await _db.JobPostings.FindAsync(cmd.JobId);
job.Status = JobStatus.InProgress;
job.HireCount++;
await _db.SaveChangesAsync();
await transaction.CommitAsync();
await _kafka.PublishAsync("contracts", contract.Id.ToString(), new ContractCreatedEvent
{
ContractId = contract.Id,
ClientId = contract.ClientId,
FreelancerId = contract.FreelancerId,
JobType = contract.JobType,
TotalAmountCents = contract.TotalAmountCents,
Currency = contract.Currency,
Milestones = cmd.Milestones.Select(m => new MilestoneDto
{
Title = m.Title, AmountCents = m.AmountCents
}).ToList(),
CreatedAt = DateTime.UtcNow
});
return contract;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
}
6. User Profiles: Freelancer & Client
Profiles are the foundation of a freelancing marketplace. A freelancer's profile is their storefront — it must convey competence, reliability, and personality in a format that clients can quickly scan. A client's profile establishes trust with freelancers, indicating payment reliability, project quality, and communication style. The profile system must support rich media (portfolio images, videos, documents), social proof (reviews, endorsements, earnings badges), and dynamic data (real-time availability, response metrics, active contracts).
Freelancer Profile Architecture
Profile Completion Score
C#public class ProfileCompletenessCalculator
{
private static readonly Dictionary<string, int> Weights = new()
{
["headline"] = 5, ["bio"] = 10, ["avatar"] = 8,
["skills_min_3"] = 12, ["skills_min_5"] = 8,
["hourly_rate"] = 10, ["portfolio_min_1"] = 15,
["portfolio_min_3"] = 10, ["work_history"] = 8,
["education"] = 5, ["timezone"] = 2,
["english_level"] = 3, ["availability"] = 4, ["verified"] = 10,
};
public static int Calculate(FreelancerProfile profile)
{
int score = 0;
if (!string.IsNullOrWhiteSpace(profile.Headline)) score += Weights["headline"];
if (!string.IsNullOrWhiteSpace(profile.Bio) && profile.Bio.Length >= 100) score += Weights["bio"];
if (!string.IsNullOrEmpty(profile.AvatarUrl)) score += Weights["avatar"];
if (profile.Skills.Count >= 3) score += Weights["skills_min_3"];
if (profile.Skills.Count >= 5) score += Weights["skills_min_5"];
if (profile.HourlyRateCents > 0) score += Weights["hourly_rate"];
if (profile.PortfolioItems.Count >= 1) score += Weights["portfolio_min_1"];
if (profile.PortfolioItems.Count >= 3) score += Weights["portfolio_min_3"];
if (profile.TotalJobsCompleted > 0) score += Weights["work_history"];
if (!string.IsNullOrEmpty(profile.EnglishProficiency)) score += Weights["english_level"];
if (profile.Availability != null) score += Weights["availability"];
if (profile.Verified) score += Weights["verified"];
return Math.Min(score, 100);
}
}
Portfolio Showcase
Portfolio items are rich content entries that let freelancers showcase their best work. Each item includes a title, description, media (images, videos, PDFs), the client and project context (optionally anonymized), technologies used, and the project URL. Portfolio items are indexed in Elasticsearch and appear in search results as rich cards. The file service handles upload, virus scanning, image optimization (WebP conversion, thumbnail generation), and CDN distribution for fast global delivery.
C#public class PortfolioItem
{
public Guid Id { get; set; }
public Guid FreelancerId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string ProjectUrl { get; set; }
public Guid? ContractId { get; set; }
public List<PortfolioMedia> Media { get; set; }
public List<string> Technologies { get; set; }
public DateOnly? ProjectDate { get; set; }
public bool IsFeatured { get; set; }
public int ViewCount { get; set; }
public DateTime CreatedAt { get; set; }
}
public class PortfolioMedia
{
public Guid Id { get; set; }
public string OriginalUrl { get; set; }
public string ThumbnailUrl { get; set; }
public string MediaType { get; set; } // image, video, document
public int DisplayOrder { get; set; }
}
7. Skill Taxonomy & Matching
The skill taxonomy is the connective tissue of the marketplace. It maps what freelancers can do to what clients need. A well-designed taxonomy enables precise search, fair comparison, market analytics, and skill-based pricing benchmarks. The taxonomy must be hierarchical (allowing broad and narrow searches), extensible (supporting new skills as technology evolves), and semantically rich (understanding that "React.js" is a type of "Frontend Development" which is a type of "Software Development").
Skill Graph Structure
Skill Matching Algorithm
The matching algorithm computes a skill similarity score between a freelancer's skills and a job's required skills. We use a weighted approach that considers direct matches, parent-category matches, and skill proficiency levels.
C#public class SkillMatcher
{
private readonly ISkillRepository _skillRepo;
public async Task<SkillMatchResult> CalculateMatch(
List<FreelancerSkill> freelancerSkills,
List<JobSkill> requiredSkills)
{
var result = new SkillMatchResult();
var skillGraph = await _skillRepo.GetSkillGraphAsync();
foreach (var required in requiredSkills)
{
var bestMatch = freelancerSkills
.Select(fs => new
{
Skill = fs,
Similarity = ComputeSimilarity(fs.SkillId, required.SkillId, skillGraph),
Weight = required.Weight
})
.OrderByDescending(x => x.Similarity)
.FirstOrDefault();
if (bestMatch != null && bestMatch.Similarity > 0)
{
result.MatchedSkills.Add(new MatchedSkill
{
RequiredSkillId = required.SkillId,
MatchedSkillId = bestMatch.Skill.SkillId,
Similarity = bestMatch.Similarity,
WeightedScore = bestMatch.Similarity * bestMatch.Weight
});
result.WeightedMatchSum += bestMatch.Similarity * bestMatch.Weight;
}
else
{
result.UnmatchedSkills.Add(required.SkillId);
}
result.TotalWeight += required.Weight;
}
result.OverallMatchPercentage = result.TotalWeight > 0
? (result.WeightedMatchSum / result.TotalWeight) * 100 : 0;
return result;
}
private double ComputeSimilarity(int skillA, int skillB, SkillGraph graph)
{
if (skillA == skillB) return 1.0;
if (graph.AreDirectMatch(skillA, skillB)) return 1.0;
var ancestorsA = graph.GetAncestors(skillA);
var ancestorsB = graph.GetAncestors(skillB);
if (ancestorsA.Contains(skillB)) return 0.8;
if (ancestorsB.Contains(skillA)) return 0.6;
var sharedAncestor = ancestorsA.Intersect(ancestorsB).FirstOrDefault();
if (sharedAncestor != null) return 0.4;
if (graph.AreSynonyms(skillA, skillB)) return 0.9;
return 0.0;
}
}
Skill Demand Analytics
We continuously analyze job postings to compute skill demand scores. These scores drive category recommendations on the homepage, skill suggestions for freelancers, and market-rate pricing data. The analytics pipeline consumes job posting events from Kafka, extracts skill tags, and computes rolling averages over 7-day, 30-day, and 90-day windows.
8. Job Posting & Proposal System
The job posting is the fundamental unit of demand on the marketplace. When a client creates a job posting, they are making a public signal about their needs, budget, and timeline. The proposal system is the supply-side response — freelancers analyze the job, craft a proposal, and compete for the work. The design of these systems directly affects marketplace liquidity (what percentage of jobs get at least one quality proposal) and match quality (what percentage of contracts result in successful outcomes).
Job Posting Lifecycle
Proposal Submission Flow
C#public class ProposalService
{
private readonly AppDbContext _db;
private readonly IKafkaProducer _kafka;
public async Task<Proposal> SubmitProposalAsync(SubmitProposalCommand cmd)
{
var dailyCount = await _db.Proposals
.CountAsync(p => p.FreelancerId == cmd.FreelancerId
&& p.CreatedAt.Date == DateTime.UtcNow.Date);
if (dailyCount >= 10)
throw new RateLimitExceededException("Daily proposal limit reached.");
var job = await _db.JobPostings.FindAsync(cmd.JobId);
if (job == null || job.Status != JobStatus.Open)
throw new InvalidOperationException("Job is no longer accepting proposals.");
if (job.ExpiresAt.HasValue && job.ExpiresAt < DateTime.UtcNow)
throw new InvalidOperationException("Job posting has expired.");
var matchResult = await _skillMatcher.CalculateMatch(
await GetFreelancerSkills(cmd.FreelancerId),
await GetJobSkills(cmd.JobId));
if (matchResult.OverallMatchPercentage < 20)
throw new InvalidOperationException(
$"Insufficient skill match ({matchResult.OverallMatchPercentage:F0}%).");
var proposal = new Proposal
{
Id = Guid.NewGuid(), JobId = cmd.JobId,
FreelancerId = cmd.FreelancerId, CoverLetter = cmd.CoverLetter,
BidAmountCents = cmd.BidAmountCents,
EstimatedDurationDays = cmd.EstimatedDurationDays,
MilestonePlan = cmd.MilestonePlan,
SkillMatchPercentage = matchResult.OverallMatchPercentage,
Status = ProposalStatus.Submitted, CreatedAt = DateTime.UtcNow
};
_db.Proposals.Add(proposal);
job.ProposalCount++;
await _db.SaveChangesAsync();
await _kafka.PublishAsync("proposals", proposal.Id.ToString(),
new ProposalSubmittedEvent
{
ProposalId = proposal.Id, JobId = cmd.JobId,
FreelancerId = cmd.FreelancerId,
BidAmountCents = cmd.BidAmountCents, CreatedAt = DateTime.UtcNow
});
return proposal;
}
}
Proposal Quality Signals
| Signal | Weight | Description |
|---|---|---|
| Skill Match % | 25% | How closely freelancer skills match job requirements |
| Job Success Score | 20% | Freelancer's historical success rate |
| Cover Letter Quality | 15% | NLP analysis of proposal text quality and relevance |
| Price Competitiveness | 15% | How bid compares to market rate for this job type |
| Response Time | 10% | How quickly the freelancer submitted the proposal |
| Availability Match | 10% | Freelancer's availability aligns with job timeline |
| Client Preference | 5% | Has the client worked with this freelancer before? |
9. Search & Ranking Engine
Search is the primary discovery mechanism in a freelancing marketplace. Clients search for freelancers by skill, rate, rating, location, and availability. Freelancers search for jobs by category, budget, skills, and client rating. The search engine must return relevant results in under 200ms while incorporating dozens of ranking signals that balance relevance, quality, freshness, and marketplace health.
Search Architecture
Ranking Formula
C#public class FreelancerRanker
{
public RankedResult ComputeRanking(
FreelancerSearchDocument doc, SearchContext context)
{
double score = 0;
double skillScore = ComputeSkillRelevance(doc.Skills, context.QuerySkills);
score += skillScore * 0.30;
double jssScore = Normalize(doc.JobSuccessScore, 0, 100);
score += jssScore * 0.20;
double ratingScore = Normalize(doc.AvgRating, 0, 5);
score += ratingScore * 0.15;
double earningsScore = Math.Log10(Math.Max(1, doc.TotalEarningsCents))
/ Math.Log10(1_000_000_000);
earningsScore = Math.Min(1.0, earningsScore);
score += earningsScore * 0.10;
double rateScore = context.HasBudget
? ComputeRateFit(doc.HourlyRateCents, context.BudgetMin, context.BudgetMax)
: 0.5;
score += rateScore * 0.10;
double availScore = doc.IsAvailableNow ? 1.0
: doc.Availability == "part_time" ? 0.5 : 0.2;
score += availScore * 0.05;
double responseScore = 1.0 - Math.Min(1.0, doc.ResponseTimeHours / 48.0);
score += responseScore * 0.05;
double freshnessScore = ComputeFreshness(doc.LastActiveAt);
score += freshnessScore * 0.05;
return new RankedResult { FreelancerId = doc.FreelancerId, Score = score };
}
private double ComputeFreshness(DateTime lastActive)
{
var hoursSinceActive = (DateTime.UtcNow - lastActive).TotalHours;
if (hoursSinceActive < 1) return 1.0;
if (hoursSinceActive < 24) return 0.9;
if (hoursSinceActive < 168) return 0.7;
if (hoursSinceActive < 720) return 0.4;
return 0.1;
}
}
Search Index Management
We use a dual-index strategy: a primary index for the full freelancer catalog and a hot index for recently active freelancers (last 30 days). The hot index is smaller and can fit in Elasticsearch memory, providing sub-50ms latency for the most common searches. The cold index is SSD-backed and handles long-tail queries for less common skills.
C#public class SearchIndexConsumer : IKafkaConsumer<UserProfileChangedEvent>
{
private readonly IElasticClient _elastic;
public async Task HandleAsync(UserProfileChangedEvent evt)
{
var doc = await BuildSearchDocument(evt.UserId);
await _elastic.IndexAsync(doc, idx => idx
.Index("freelancers").Id(evt.UserId).Refresh(Refresh.WaitFor));
}
private async Task<FreelancerSearchDocument> BuildSearchDocument(Guid userId)
{
var profile = await _db.FreelancerProfiles
.Include(p => p.User)
.Include(p => p.Skills).ThenInclude(s => s.Skill)
.Include(p => p.Reviews)
.FirstOrDefaultAsync(p => p.UserId == userId);
return new FreelancerSearchDocument
{
FreelancerId = userId,
FullName = profile.User.FullName,
Headline = profile.Headline,
Bio = profile.Bio,
Skills = profile.Skills.Select(s => new SkillDoc
{
Name = s.Skill.Name, Slug = s.Skill.Slug,
Proficiency = s.Proficiency, Years = s.YearsUsed,
Category = s.Skill.Category
}).ToList(),
HourlyRateCents = profile.HourlyRateCents ?? 0,
JobSuccessScore = profile.JobSuccessScore,
TotalEarningsCents = profile.TotalEarningsCents,
TotalJobs = profile.TotalJobsCompleted,
AvgRating = profile.AvgRating,
ReviewCount = profile.ReviewCount,
ResponseTimeHours = profile.ResponseTimeHours,
Availability = profile.Availability,
Country = profile.User.CountryCode,
EnglishProficiency = profile.EnglishProficiency,
LastActiveAt = profile.LastActiveAt,
Verified = profile.Verified
};
}
}
10. Bidding & Auction System
The bidding system determines how clients and freelancers agree on price and scope. A well-designed bidding system maximizes value for both parties while preventing manipulation. We support multiple bidding models: fixed-price bidding (freelancers submit bids, client selects), hourly bidding (freelancers propose hourly rates), and reverse auction (client sets budget, freelancers compete on price).
Bidding Models Comparison
| Model | How It Works | Best For | Risks |
|---|---|---|---|
| Open Bidding | Freelancers submit bids with price and proposal | Fixed-price projects | Race to bottom, spam proposals |
| Sealed Bid | Freelancers submit bids visible only to client | High-value contracts | Less price discovery |
| Reverse Auction | Client sets budget, freelancers lower price | Simple tasks | Quality degradation |
| Hourly Rate | Freelancer proposes hourly rate | Ongoing work | Scope creep, hour inflation |
Bid Validation & Anti-Fraud
C#public class BidValidator
{
private readonly IMarketPricingService _pricingService;
public async Task<ValidationResult> ValidateBidAsync(CreateBidCommand cmd)
{
var errors = new List<string>();
if (cmd.AmountCents <= 0)
errors.Add("Bid amount must be positive.");
var job = await _jobRepo.GetByIdAsync(cmd.JobId);
if (job.BudgetMaxCents.HasValue && cmd.AmountCents > job.BudgetMaxCents.Value)
errors.Add("Bid exceeds client's maximum budget.");
if (job.BudgetMinCents.HasValue && cmd.AmountCents < job.BudgetMinCents.Value)
errors.Add("Bid is below client's minimum budget.");
var marketRate = await _pricingService.GetMarketRateAsync(
job.RequiredSkills, job.ExperienceLevel, job.Country);
var profile = await _profileRepo.GetByIdAsync(cmd.FreelancerId);
if (cmd.AmountCents < marketRate.P25 * 0.5)
errors.Add("Your bid is significantly below market rate.");
if (cmd.AmountCents > marketRate.P95 * 2)
errors.Add("Your bid is significantly above market rate.");
var existingBid = await _bidRepo.GetByJobAndFreelancerAsync(
cmd.JobId, cmd.FreelancerId);
if (existingBid != null)
errors.Add("You have already submitted a bid for this job.");
return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
}
}
11. Milestone-Based Payments & Escrow
Payment trust is the single most important feature of a freelancing marketplace. Without a reliable payment system, clients fear paying for work not delivered, and freelancers fear working without payment. The escrow system solves this by holding client funds in a trusted intermediary account and releasing them only when work is accepted. Our escrow system is built on Stripe Connect's managed account model.
Payment Flow
Escrow State Machine
C#public enum EscrowStatus
{
PendingFunding, Held, InReview, Disputed,
Released, Refunded, PartiallyReleased
}
public class EscrowStateMachine
{
private static readonly Dictionary<EscrowStatus, HashSet<EscrowStatus>>
Transitions = new()
{
[EscrowStatus.PendingFunding] = new() { EscrowStatus.Held },
[EscrowStatus.Held] = new()
{ EscrowStatus.InReview, EscrowStatus.Disputed, EscrowStatus.Refunded },
[EscrowStatus.InReview] = new()
{ EscrowStatus.Released, EscrowStatus.Disputed, EscrowStatus.Held },
[EscrowStatus.Disputed] = new()
{ EscrowStatus.Released, EscrowStatus.Refunded,
EscrowStatus.PartiallyReleased },
[EscrowStatus.Released] = new(),
[EscrowStatus.Refunded] = new(),
[EscrowStatus.PartiallyReleased] = new()
{ EscrowStatus.Released, EscrowStatus.Refunded }
};
public void ValidateTransition(EscrowStatus current, EscrowStatus next)
{
if (!Transitions[current].Contains(next))
throw new InvalidEscrowTransitionException(
$"Cannot transition from {current} to {next}.");
}
}
Auto-Release Logic
C#public class EscrowAutoReleaseService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var pendingReview = await _db.EscrowTransactions
.Where(e => e.Status == EscrowStatus.InReview
&& e.SubmittedForReviewAt != null
&& EF.Functions.DateDiffDay(
e.SubmittedForReviewAt, DateTime.UtcNow) >= 14)
.ToListAsync();
foreach (var escrow in pendingReview)
{
await _escrowService.ReleaseAsync(escrow.Id,
"Auto-release: client did not respond within 14 days");
}
var unfunded = await _db.Milestones
.Where(m => m.Status == MilestoneStatus.Approved
&& m.FundedAt == null
&& EF.Functions.DateDiffDay(m.CreatedAt, DateTime.UtcNow) >= 7)
.ToListAsync();
foreach (var milestone in unfunded)
{
await _contractService.CancelMilestoneAsync(milestone.Id);
}
await Task.Delay(TimeSpan.FromMinutes(1), ct);
}
}
}
12. Time Tracking & Hourly Billing
For hourly contracts, accurate time tracking is essential for trust. The platform provides a desktop time tracker that captures screenshots at configurable intervals (every 10 minutes by default), records keyboard/mouse activity levels, and tracks which application the freelancer was working in. Hours are aggregated into weekly timesheets that clients can review and approve before payment is processed.
C#public class TimeEntry
{
public Guid Id { get; set; }
public Guid ContractId { get; set; }
public Guid FreelancerId { get; set; }
public DateTime StartTime { get; set; }
public DateTime? EndTime { get; set; }
public int DurationMinutes { get; set; }
public string Description { get; set; }
public List<string> ScreenshotUrls { get; set; }
public int ActivityLevel { get; set; }
public string Status { get; set; }
public bool? Approved { get; set; }
}
public class WeeklyTimesheet
{
public Guid ContractId { get; set; }
public Guid FreelancerId { get; set; }
public DateTime WeekStart { get; set; }
public DateTime WeekEnd { get; set; }
public List<TimeEntry> Entries { get; set; }
public int TotalMinutes => Entries.Sum(e => e.DurationMinutes);
public decimal TotalHours => TotalMinutes / 60m;
}
public class TimesheetService
{
public async Task<WeeklyTimesheet> ApproveTimesheetAsync(
Guid contractId, DateTime weekStart, Guid approverId)
{
var entries = await _db.TimeEntries
.Where(t => t.ContractId == contractId
&& t.StartTime >= weekStart
&& t.StartTime < weekStart.AddDays(7)
&& t.Status == "tracked")
.ToListAsync();
ValidateNoOverlaps(entries);
foreach (var entry in entries)
{
if (entry.ActivityLevel < 10 && entry.DurationMinutes > 30)
throw new LowActivityWarningException(
$"Entry on {entry.StartTime:MMM dd} has only {entry.ActivityLevel}% activity.");
entry.Status = "approved";
entry.Approved = true;
entry.ApprovedBy = approverId;
entry.ApprovedAt = DateTime.UtcNow;
}
await _db.SaveChangesAsync();
var contract = await _db.Contracts.FindAsync(contractId);
var totalAmount = entries.Sum(e => e.DurationMinutes) / 60m * contract.HourlyRateCents;
await _paymentService.ProcessHourlyPaymentAsync(contractId, entries, totalAmount);
return await BuildTimesheetAsync(contractId, weekStart);
}
}
13. Dispute Resolution Workflow
Disputes are inevitable in any marketplace. A well-designed dispute resolution system must be fair, transparent, timely, and proportionate. Our system handles disputes at multiple levels: automated resolution for clear-cut cases, human mediation for complex cases, and arbitration for high-value disputes.
Dispute Lifecycle
Dispute Resolution Rules
C#public class DisputeResolver
{
public async Task<DisputeResolution> EvaluateDisputeAsync(Dispute dispute)
{
var evidence = await _db.DisputeEvidence
.Where(e => e.DisputeId == dispute.Id).ToListAsync();
var contract = await _db.Contracts
.Include(c => c.Milestones)
.FirstOrDefaultAsync(c => c.Id == dispute.ContractId);
if (dispute.Reason == "late_delivery"
&& contract.Milestones.All(m => m.Status == "pending"))
{
return new DisputeResolution
{
Decision = DisputeDecision.RefundToClient,
AmountCents = dispute.AmountCents,
Reason = "No milestones delivered. Full refund.",
AutoResolved = true
};
}
if (dispute.Reason == "quality_issue"
&& contract.Milestones.Any(m =>
m.Status == "approved" && m.Id == dispute.MilestoneId))
{
return new DisputeResolution
{
Decision = DisputeDecision.UpholdFreelancer,
AmountCents = dispute.AmountCents,
Reason = "Milestone was previously approved.",
AutoResolved = true
};
}
if (dispute.Reason == "work_not_delivered")
{
var lastActivity = await _db.TimeEntries
.Where(t => t.ContractId == dispute.ContractId)
.MaxAsync(t => (DateTime?)t.StartTime);
if (lastActivity == null || (DateTime.UtcNow - lastActivity.Value).Days > 14)
{
return new DisputeResolution
{
Decision = DisputeDecision.RefundToClient,
AmountCents = dispute.AmountCents,
Reason = "No freelancer activity for 14+ days.",
AutoResolved = true
};
}
}
return new DisputeResolution
{
Decision = DisputeDecision.PendingMediation,
RequiresHumanReview = true,
Priority = dispute.AmountCents > 100000 ? "high" : "normal"
};
}
}
14. Review & Rating System
Reviews are the reputation backbone of the marketplace. A fair, transparent review system builds trust and helps both parties make informed decisions. Our system is two-sided: after contract completion, both the freelancer and the client are prompted to leave reviews within 14 days. Reviews are multi-dimensional (communication, quality, timeliness, expertise) and include both numerical ratings and text comments.
Review Computation
C#public class ReviewService
{
public async Task<ReviewStats> GetReviewStatsAsync(Guid userId)
{
var reviews = await _db.Reviews
.Where(r => r.RevieweeId == userId && r.IsPublic)
.ToListAsync();
if (!reviews.Any()) return new ReviewStats { HasReviews = false };
var weightedAvg = reviews.Average(r =>
r.Communication * 0.2 + r.Quality * 0.3 +
r.Timeliness * 0.2 + r.Expertise * 0.2 + r.Overall * 0.1);
var recencyWeightedAvg = reviews
.OrderByDescending(r => r.CreatedAt)
.Select((r, i) => new { Review = r, Weight = Math.Pow(0.95, i) })
.Sum(x => x.Review.Overall * x.Weight)
/ reviews.Select((r, i) => Math.Pow(0.95, i)).Sum();
return new ReviewStats
{
HasReviews = true,
AverageRating = Math.Round(weightedAvg, 2),
RecencyWeightedRating = Math.Round(recencyWeightedAvg, 2),
TotalReviews = reviews.Count,
Distribution = new Dictionary<int, int>
{
[5] = reviews.Count(r => r.Overall == 5),
[4] = reviews.Count(r => r.Overall == 4),
[3] = reviews.Count(r => r.Overall == 3),
[2] = reviews.Count(r => r.Overall == 2),
[1] = reviews.Count(r => r.Overall == 1)
},
DimensionAverages = new DimensionAverages
{
Communication = reviews.Average(r => r.Communication),
Quality = reviews.Average(r => r.Quality),
Timeliness = reviews.Average(r => r.Timeliness),
Expertise = reviews.Average(r => r.Expertise)
}
};
}
}
Review Integrity Measures
- Timed Reviews: Both parties have 14 days to submit. After 14 days, the review window closes and no review can be left. This prevents vindictive late reviews.
- Mutual Disclosure: Reviews are revealed simultaneously (double-blind). Neither party sees the other's review until both have submitted or the window closes.
- No Edit/Delete: Once submitted, reviews cannot be edited or deleted. This maintains the integrity of the review record.
- Fraud Detection: We detect review manipulation patterns: too many 5-star reviews in a short period, reviews from the same IP/network, reviews where the contract value was unusually low.
- Response Rights: The reviewed party can post a public response to any review, providing their perspective without modifying the original review.
15. Contract Management
A contract is the formal agreement between a client and freelancer that governs the work relationship. It captures scope, payment terms, milestones, deadlines, and governing policies. The contract service manages the full lifecycle from creation through completion, including state transitions, amendments, and termination.
C#public enum ContractStatus
{
PendingActivation, Active, OnHold, InReview,
Completed, Cancelled, Disputed, Expired
}
public class ContractStateMachine
{
private static readonly Dictionary<ContractStatus, HashSet<ContractStatus>>
AllowedTransitions = new()
{
[ContractStatus.PendingActivation] = new()
{ ContractStatus.Active, ContractStatus.Cancelled, ContractStatus.Expired },
[ContractStatus.Active] = new()
{ ContractStatus.InReview, ContractStatus.OnHold,
ContractStatus.Cancelled, ContractStatus.Completed,
ContractStatus.Disputed, ContractStatus.Expired },
[ContractStatus.OnHold] = new()
{ ContractStatus.Active, ContractStatus.Cancelled, ContractStatus.Expired },
[ContractStatus.InReview] = new()
{ ContractStatus.Active, ContractStatus.Completed,
ContractStatus.Disputed, ContractStatus.Cancelled },
[ContractStatus.Completed] = new(),
[ContractStatus.Cancelled] = new(),
[ContractStatus.Disputed] = new()
{ ContractStatus.Active, ContractStatus.Completed, ContractStatus.Cancelled },
[ContractStatus.Expired] = new() { ContractStatus.Active }
};
}
16. Messaging & File Sharing
Communication is the lifeblood of a freelancing relationship. The messaging system must support real-time chat between freelancers and clients, file sharing (documents, designs, code), read receipts, message search, and message history for dispute resolution. We use a WebSocket-based architecture for real-time delivery with PostgreSQL for persistence and Redis for presence tracking.
Messaging Architecture
C#public class MessageService
{
private readonly AppDbContext _db;
private readonly IRedisService _redis;
private readonly IWebSocketHub _wsHub;
private readonly INotificationService _notifService;
public async Task<Message> SendMessageAsync(SendMessageCommand cmd)
{
var conversation = await GetOrCreateConversationAsync(
cmd.SenderId, cmd.RecipientId);
var message = new Message
{
Id = Guid.NewGuid(),
ConversationId = conversation.Id,
SenderId = cmd.SenderId,
Content = cmd.Content,
MessageType = cmd.MessageType ?? "text",
AttachmentUrls = cmd.AttachmentUrls ?? new List<string>(),
CreatedAt = DateTime.UtcNow
};
_db.Messages.Add(message);
conversation.LastMessageAt = DateTime.UtcNow;
conversation.LastMessagePreview = cmd.Content.Length > 100
? cmd.Content[..100] + "..." : cmd.Content;
await _db.SaveChangesAsync();
await _wsHub.SendToUserAsync(cmd.RecipientId, "new_message", new
{
MessageId = message.Id,
ConversationId = conversation.Id,
SenderId = cmd.SenderId,
SenderName = cmd.SenderName,
Content = message.Content,
CreatedAt = message.CreatedAt
});
await _redis.SetAsync(
$"conversation:{conversation.Id}:last_active",
DateTime.UtcNow.ToString("O"), TimeSpan.FromDays(30));
if (cmd.Notify)
{
await _notifService.SendAsync(cmd.RecipientId, "New Message",
$"{cmd.SenderName}: {cmd.Content[..Math.Min(50, cmd.Content.Length)]}...");
}
return message;
}
public async Task MarkAsReadAsync(Guid conversationId, Guid userId)
{
var unreadMessages = await _db.Messages
.Where(m => m.ConversationId == conversationId
&& m.SenderId != userId
&& m.ReadAt == null)
.ToListAsync();
foreach (var msg in unreadMessages) msg.ReadAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
await _wsHub.SendToUserAsync(userId, "messages_read", new
{
ConversationId = conversationId,
ReadAt = DateTime.UtcNow
});
}
}
File Sharing
Files are uploaded to S3 via pre-signed URLs (avoiding server-side proxying), virus-scanned with ClamAV, and served through CloudFront CDN. Supported file types include images (JPG, PNG, GIF, WebP), documents (PDF, DOCX, XLSX), archives (ZIP, RAR), and code files. File size limits are 50MB per file and 200MB per message. All uploaded files are encrypted at rest (AES-256) and in transit (TLS 1.3).
C#public class FileService
{
private readonly IAmazonS3 _s3;
private readonly IVirusScanner _virusScanner;
public async Task<UploadUrlResponse> GetPresignedUploadUrlAsync(
string fileName, string contentType, Guid userId)
{
var key = $"uploads/{userId}/{Guid.NewGuid()}/{fileName}";
var request = new GetPreSignedUrlRequest
{
BucketName = _config.BucketName,
Key = key,
Expires = DateTime.UtcNow.AddMinutes(15),
Verb = HttpVerb.PUT,
ContentType = contentType
};
var url = await _s3.GetPreSignedURLAsync(request);
return new UploadUrlResponse { UploadUrl = url, Key = key, ExpiresIn = 900 };
}
public async Task<FileMetadata> ProcessUploadedFileAsync(string key)
{
var scanResult = await _virusScanner.ScanAsync(key);
if (scanResult.IsInfected)
{
await _s3.DeleteObjectAsync(_config.BucketName, key);
throw new VirusDetectedException($"File {key} failed virus scan.");
}
var metadata = await GetFileMetadataAsync(key);
if (metadata.ContentType.StartsWith("image/"))
{
await GenerateThumbnailsAsync(key);
await ConvertToWebPAsync(key);
}
return metadata;
}
}
17. Talent Pools & Shortlisting
Talent pools let clients curate private lists of freelancers for recurring projects. Instead of posting a new job and waiting for proposals each time, a client can post directly to their talent pool and get proposals from pre-vetted freelancers. This reduces time-to-hire from days to hours and increases match quality because the client already knows and trusts these freelancers.
C#public class TalentPool
{
public Guid Id { get; set; }
public Guid ClientId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Visibility { get; set; } // private, shared, public
public List<TalentPoolMember> Members { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public class TalentPoolMember
{
public Guid TalentPoolId { get; set; }
public Guid FreelancerId { get; set; }
public string AddedReason { get; set; }
public int Priority { get; set; }
public DateTime AddedAt { get; set; }
public string Notes { get; set; }
}
public class TalentPoolService
{
public async Task<List<TalentPoolMatch>> FindMatchesForJobAsync(
Guid talentPoolId, Guid jobId)
{
var pool = await _db.TalentPools
.Include(p => p.Members).ThenInclude(m => m.FreelancerProfile)
.FirstOrDefaultAsync(p => p.Id == talentPoolId);
var job = await _db.JobPostings.FindAsync(jobId);
var matches = new List<TalentPoolMatch>();
foreach (var member in pool.Members)
{
var skillMatch = await _skillMatcher.CalculateMatch(
member.FreelancerProfile.Skills, job.RequiredSkills);
matches.Add(new TalentPoolMatch
{
FreelancerId = member.FreelancerId,
FreelancerName = member.FreelancerProfile.User.FullName,
SkillMatchPercentage = skillMatch.OverallMatchPercentage,
HourlyRate = member.FreelancerProfile.HourlyRateCents,
JobSuccessScore = member.FreelancerProfile.JobSuccessScore,
IsAvailable = member.FreelancerProfile.Availability != "not_available",
Priority = member.Priority,
PastContracts = await _db.Contracts
.CountAsync(c => c.FreelancerId == member.FreelancerId
&& c.ClientId == pool.ClientId)
});
}
return matches.OrderByDescending(m => m.SkillMatchPercentage)
.ThenByDescending(m => m.Priority).ToList();
}
public async Task SendInviteAsync(Guid talentPoolId, List<Guid> freelancerIds,
Guid jobId)
{
var job = await _db.JobPostings.FindAsync(jobId);
foreach (var freelancerId in freelancerIds)
{
await _notifService.SendAsync(freelancerId, "Talent Pool Invitation",
$"You've been invited to apply for: {job.Title}");
await _db.Invitations.AddAsync(new Invitation
{
JobId = jobId, FreelancerId = freelancerId,
Status = "pending", ExpiresAt = DateTime.UtcNow.AddDays(7)
});
}
await _db.SaveChangesAsync();
}
}
Shortlisting Algorithm
When a client posts a new job, the system automatically identifies shortlisted freelancers from the talent pool and past successful collaborations. The shortlisting algorithm considers: skill match percentage, past contract outcomes, client preference history, availability, and rate compatibility. Top candidates receive push notifications inviting them to submit proposals, while the client sees a "Recommended" badge on these proposals.
18. Payment Processing, Payouts & Tax
Payment processing is the most complex and highest-stakes subsystem of the marketplace. Errors in payment processing directly cause financial loss, regulatory violations, and loss of user trust. We use Stripe Connect as the payment infrastructure, handling the complexity of multi-party payments, currency conversion, KYC, and regulatory compliance across 180+ countries.
Stripe Connect Integration
C#public class StripePaymentService
{
private readonly StripeClient _stripe;
public async Task<string> CreateConnectedAccountAsync(
Guid userId, string country, string accountType)
{
var createOptions = new AccountCreateOptions
{
Country = country,
Type = accountType == "freelancer" ? "express" : "standard",
Capabilities = new AccountCapabilitiesOptions
{
CardPayments = new AccountCapabilitiesCardPaymentsOptions
{
Requested = true
},
Transfers = new AccountCapabilitiesTransfersOptions
{
Requested = true
}
},
Metadata = new Dictionary<string, string>
{
["user_id"] = userId.ToString()
}
};
var account = await _stripe.Accounts.CreateAsync(createOptions);
await UpdateUserStripeAccountId(userId, account.Id);
return account.Id;
}
public async Task<EscrowResult> FundEscrowAsync(
Guid clientId, long amountCents, string currency, Guid milestoneId)
{
var client = await _db.Users.FindAsync(clientId);
var paymentIntent = await _stripe.PaymentIntents.CreateAsync(
new PaymentIntentCreateOptions
{
Amount = amountCents,
Currency = currency.ToLower(),
Customer = client.StripeCustomerId,
ApplicationFeeAmount = (long)(amountCents * 0.10),
TransferData = new PaymentIntentTransferDataOptions
{
Destination = _config.PlatformConnectedAccountId
},
Metadata = new Dictionary<string, string>
{
["milestone_id"] = milestoneId.ToString(),
["type"] = "escrow_funding"
},
IdempotencyKey = $"escrow-{milestoneId}-{DateTime.UtcNow.Ticks}"
});
return new EscrowResult
{
PaymentIntentId = paymentIntent.Id,
Status = paymentIntent.Status,
ClientSecret = paymentIntent.ClientSecret
};
}
public async Task<TransferResult> ReleaseToFreelancerAsync(
Guid freelancerId, long amountCents, string currency, Guid escrowId)
{
var freelancer = await _db.Users.FindAsync(freelancerId);
var transfer = await _stripe.Transfers.CreateAsync(
new TransferCreateOptions
{
Amount = amountCents,
Currency = currency.ToLower(),
Destination = freelancer.StripeConnectedAccountId,
Metadata = new Dictionary<string, string>
{
["escrow_id"] = escrowId.ToString()
},
IdempotencyKey = $"release-{escrowId}-{DateTime.UtcNow.Ticks}"
});
return new TransferResult { TransferId = transfer.Id, Status = "completed" };
}
}
Payout Scheduling
Freelancers can choose from several payout schedules: instant (1-2% fee), daily, weekly, or monthly. The payout service aggregates completed milestones and processes batch payouts through Stripe Connect. We maintain a double-entry ledger that tracks every financial transaction: client payment to escrow, escrow hold, escrow release, platform fee deduction, and freelancer payout.
C#public class PayoutScheduler : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var eligibleFreelancers = await _db.Users
.Where(u => u.PayoutSchedule == "daily"
&& u.StripeConnectedAccountId != null)
.ToListAsync();
foreach (var freelancer in eligibleFreelancers)
{
var pendingAmount = await _db.EscrowTransactions
.Where(e => e.FreelancerId == freelancer.Id
&& e.Status == EscrowStatus.Released
&& e.PaidOut == false)
.SumAsync(e => e.AmountCents);
if (pendingAmount > 100) // Minimum $1.00 payout
{
await ProcessPayoutAsync(freelancer, pendingAmount);
}
}
await Task.Delay(TimeSpan.FromHours(1), ct);
}
}
private async Task ProcessPayoutAsync(User freelancer, long amountCents)
{
var transfer = await _stripe.Transfers.CreateAsync(new TransferCreateOptions
{
Amount = amountCents,
Currency = "usd",
Destination = freelancer.StripeConnectedAccountId,
IdempotencyKey = $"payout-{freelancer.Id}-{DateTime.UtcNow:yyyyMMdd}"
});
var pendingEscrows = await _db.EscrowTransactions
.Where(e => e.FreelancerId == freelancer.Id
&& e.Status == EscrowStatus.Released && !e.PaidOut)
.ToListAsync();
foreach (var escrow in pendingEscrows) escrow.PaidOut = true;
await _db.SaveChangesAsync();
}
}
Tax Documentation
For US freelancers earning over $600/year, the platform is legally required to issue Form 1099-NEC. We collect W-9 information during onboarding and generate 1099s annually using a tax preparation API integration. For international freelancers, we handle country-specific requirements: VAT invoices for EU freelancers, TDS certificates for Indian freelancers, and withholding certificates for Canadian freelancers. The tax service maintains a comprehensive ledger of all earnings, fees, and payouts per user per tax year.
C#public class TaxDocumentService
{
public async Task GenerateAnnual1099Async(int taxYear)
{
var usFreelancers = await _db.Users
.Where(u => u.CountryCode == "US"
&& u.Role != "client")
.ToListAsync();
foreach (var freelancer in usFreelancers)
{
var annualEarnings = await _db.EscrowTransactions
.Where(e => e.FreelancerId == freelancer.Id
&& e.Status == EscrowStatus.Released
&& e.ReleasedAt.Value.Year == taxYear)
.SumAsync(e => e.AmountCents);
if (annualEarnings < 60000) continue; // Below $600 threshold
var doc = new TaxDocument
{
UserId = freelancer.Id,
TaxYear = taxYear,
DocumentType = "1099-NEC",
TotalEarningsCents = annualEarnings,
TotalFeesCents = await _db.Invoices
.Where(i => i.FreelancerId == freelancer.Id
&& i.IssuedAt.Value.Year == taxYear)
.SumAsync(i => i.FeeCents),
PayerEIN = _config.PlatformEIN,
PayerName = _config.PlatformLegalName,
RecipientTIN = freelancer.TaxIdEncrypted,
RecipientName = freelancer.FullName,
RecipientAddress = freelancer.Address,
Status = "pending_generation",
CreatedAt = DateTime.UtcNow
};
_db.TaxDocuments.Add(doc);
}
await _db.SaveChangesAsync();
await _taxApi.SubmitBatchAsync(await _db.TaxDocuments
.Where(t => t.TaxYear == taxYear && t.Status == "pending_generation")
.ToListAsync());
}
}
Fee Structure
| Fee Type | Amount | Applies To |
|---|---|---|
| Service Fee (Client) | 3-5% of payment | All client payments |
| Service Fee (Freelancer) | 10% flat or sliding scale | All freelancer earnings |
| Instant Withdrawal | 1-2% of payout | Freelancers choosing instant payout |
| Currency Conversion | 1.5% above mid-market rate | Cross-currency transactions |
| Escrow Administration | Included in service fee | All milestone-funded contracts |
| Dispute Resolution | $15 processing fee | Disputes escalated to mediation |
19. Identity Verification & Skill Assessments
Trust on a marketplace begins with identity verification. Before a freelancer can receive payments or a client can post jobs requiring escrow, they must complete a KYC (Know Your Customer) verification process. This protects against fraud, money laundering, and identity theft while satisfying regulatory requirements in multiple jurisdictions.
Verification Flow
C#public class IdentityVerificationService
{
private readonly IKycProvider _kycProvider;
public async Task<VerificationResult> StartVerificationAsync(
Guid userId, string documentType, string documentFrontUrl,
string documentBackUrl = null, string selfieUrl = null)
{
var verification = new IdentityVerification
{
Id = Guid.NewGuid(),
UserId = userId,
DocumentType = documentType,
DocumentFrontUrl = documentFrontUrl,
DocumentBackUrl = documentBackUrl,
SelfieUrl = selfieUrl,
Status = "processing",
CreatedAt = DateTime.UtcNow
};
_db.IdentityVerifications.Add(verification);
await _db.SaveChangesAsync();
var kycResult = await _kycProvider.VerifyAsync(new KycRequest
{
DocumentFrontUrl = documentFrontUrl,
DocumentBackUrl = documentBackUrl,
SelfieUrl = selfieUrl,
CallbackUrl = $"https://api.platform.com/webhooks/kyc/{verification.Id}"
});
verification.KycReferenceId = kycResult.ReferenceId;
verification.Status = kycResult.Status; // approved, pending, rejected
if (kycResult.Status == "approved")
{
var user = await _db.Users.FindAsync(userId);
user.IdentityVerified = true;
user.VerificationLevel = documentBackUrl != null ? "enhanced" : "basic";
}
await _db.SaveChangesAsync();
return new VerificationResult
{
VerificationId = verification.Id,
Status = verification.Status,
Message = kycResult.Message
};
}
}
Skill Assessments
Skill assessments validate freelancer capabilities beyond what their profile claims. We offer timed, proctored tests for major skill categories. Assessment scores are displayed on freelancer profiles and factored into search ranking. The assessment system must be resistant to cheating (randomized questions, IP-based duplicate detection, browser lockdown) and regularly updated with new questions to prevent dumps.
C#public class SkillAssessment
{
public Guid Id { get; set; }
public Guid FreelancerId { get; set; }
public int SkillId { get; set; }
public string AssessmentVersion { get; set; }
public int TimeLimitMinutes { get; set; }
public int TotalQuestions { get; set; }
public int CorrectAnswers { get; set; }
public decimal Percentile { get; set; }
public string ProctoringData { get; set; }
public DateTime StartedAt { get; set; }
public DateTime CompletedAt { get; set; }
public string Status { get; set; } // in_progress, completed, failed, expired
public bool IsPassed => Percentile >= 40;
}
public class AssessmentService
{
public async Task<AssessmentResult> SubmitAssessmentAsync(
Guid assessmentId, List<Answer> answers)
{
var assessment = await _db.SkillAssessments.FindAsync(assessmentId);
var questions = await _questionBank.GetQuestionsAsync(
assessment.SkillId, assessment.AssessmentVersion);
int correct = 0;
foreach (var answer in answers)
{
var question = questions.First(q => q.Id == answer.QuestionId);
if (question.CorrectAnswer == answer.SelectedAnswer) correct++;
}
assessment.CorrectAnswers = correct;
var percentile = await ComputePercentileAsync(
assessment.SkillId, correct, assessment.TotalQuestions);
assessment.Percentile = percentile;
assessment.CompletedAt = DateTime.UtcNow;
assessment.Status = "completed";
await _db.SaveChangesAsync();
return new AssessmentResult
{
Score = correct,
TotalQuestions = assessment.TotalQuestions,
Percentile = percentile,
Passed = assessment.IsPassed
};
}
}
20. API Design
The platform exposes a RESTful API for web and mobile clients, with a public API for third-party integrations. All API endpoints follow consistent conventions: plural nouns for resources, HTTP verbs for actions, cursor-based pagination, and standardized error responses. Rate limiting is applied per-user with separate limits for authenticated and anonymous requests.
API Endpoint Overview
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/v1/auth/register | Register new user | Public |
| POST | /api/v1/auth/login | Login and get JWT | Public |
| GET | /api/v1/freelancers | Search freelancers | Public |
| GET | /api/v1/freelancers/{id} | Get freelancer profile | Public |
| PUT | /api/v1/freelancers/me | Update own profile | Freelancer |
| GET | /api/v1/jobs | Search jobs | Public |
| POST | /api/v1/jobs | Create job posting | Client |
| GET | /api/v1/jobs/{id} | Get job details | Public |
| POST | /api/v1/jobs/{id}/proposals | Submit proposal | Freelancer |
| GET | /api/v1/proposals | List own proposals | Freelancer |
| PUT | /api/v1/proposals/{id}/accept | Accept proposal | Client |
| GET | /api/v1/contracts | List own contracts | Authenticated |
| POST | /api/v1/contracts/{id}/milestones | Add milestone | Client |
| POST | /api/v1/milestones/{id}/fund | Fund milestone (escrow) | Client |
| POST | /api/v1/milestones/{id}/deliver | Submit delivery | Freelancer |
| POST | /api/v1/milestones/{id}/approve | Approve and release | Client |
| GET | /api/v1/messages/conversations | List conversations | Authenticated |
| POST | /api/v1/messages | Send message | Authenticated |
| POST | /api/v1/reviews | Submit review | Authenticated |
| GET | /api/v1/reviews/{userId} | Get user reviews | Public |
| POST | /api/v1/disputes | Initiate dispute | Authenticated |
| GET | /api/v1/payments/history | Payment history | Authenticated |
| GET | /api/v1/invoices | List invoices | Authenticated |
| POST | /api/v1/portfolio | Add portfolio item | Freelancer |
| GET | /api/v1/skills | Browse skill taxonomy | Public |
| POST | /api/v1/assessments/start | Start skill assessment | Freelancer |
| POST | /api/v1/assessments/{id}/submit | Submit assessment | Freelancer |
| POST | /api/v1/identity/verify | Start identity verification | Authenticated |
Standardized Error Response
C#public class ApiResponse<T>
{
public bool Success { get; set; }
public T Data { get; set; }
public List<ApiError> Errors { get; set; }
public ApiResponseMeta Meta { get; set; }
}
public class ApiError
{
public string Code { get; set; }
public string Message { get; set; }
public string Field { get; set; }
}
public class ApiResponseMeta
{
public string RequestId { get; set; }
public long Timestamp { get; set; }
public int? TotalCount { get; set; }
public string NextCursor { get; set; }
public int RateLimitRemaining { get; set; }
}
Public API Authentication
Third-party integrations use OAuth 2.0 client credentials flow. API keys are scoped to specific permissions (read:freelancers, write:jobs, read:contracts) and rate-limited independently. The public API supports webhook subscriptions for event-driven integrations — clients can subscribe to contract.status_changed, payment.milestone_released, or message.received events.
21. Security, Compliance & Monitoring
A marketplace handling financial transactions and personal data must implement defense-in-depth security. Our security posture covers data protection (encryption at rest and in transit), access control (RBAC and ABAC), fraud detection, regulatory compliance (GDPR, CCPA, PCI-DSS), and incident response. Every security control is documented, tested, and audited quarterly.
Security Architecture
Security Controls
| Control | Implementation | Scope |
|---|---|---|
| Authentication | JWT (15 min) + Refresh tokens (30 days), OAuth2/Google/GitHub | All API access |
| Authorization | Role-based (freelancer, client, admin) + resource-level ownership checks | All endpoints |
| Encryption at Rest | AES-256 for DB, S3, EBS; KMS-managed keys with rotation | All data stores |
| Encryption in Transit | TLS 1.3 everywhere, certificate pinning on mobile | All network traffic |
| Secrets Management | AWS Secrets Manager, no secrets in code or env vars | All credentials |
| Input Validation | Server-side validation on all inputs, parameterized queries | All user inputs |
| SQL Injection Prevention | Entity Framework parameterized queries, no raw SQL from user input | All DB access |
| XSS Prevention | Content Security Policy headers, HTML sanitization (DOMPurify) | All rendered content |
| CSRF Protection | SameSite cookies, CSRF tokens for state-changing operations | All forms |
| Rate Limiting | Sliding window rate limiter per user/IP, separate limits per endpoint category | All API endpoints |
| Audit Logging | All state changes logged with actor, timestamp, before/after values | All financial and admin actions |
| DDoS Protection | AWS Shield Advanced, CloudFront rate limiting, ALB WAF rules | Perimeter |
| Vulnerability Scanning | Snyk for dependencies, Trivy for containers, weekly scans | All code and images |
| Penetration Testing | Annual third-party pentest + quarterly internal red team exercises | Full platform |
GDPR & CCPA Compliance
C#public class DataPrivacyService
{
public async Task<DataExportResult> ExportUserDataAsync(Guid userId)
{
var user = await _db.Users.FindAsync(userId);
var profile = await _db.FreelancerProfiles
.Include(p => p.Skills).FirstOrDefaultAsync(p => p.UserId == userId);
var contracts = await _db.Contracts
.Where(c => c.ClientId == userId || c.FreelancerId == userId)
.ToListAsync();
var messages = await _db.Messages
.Where(m => m.SenderId == userId)
.ToListAsync();
var reviews = await _db.Reviews
.Where(r => r.ReviewerId == userId || r.RevieweeId == userId)
.ToListAsync();
var export = new UserDataExport
{
UserProfile = new { user.FullName, user.Email, user.CountryCode },
FreelancerProfile = profile != null ? new
{
profile.Headline, profile.Bio, profile.HourlyRateCents
} : null,
Skills = profile?.Skills.Select(s => s.Skill.Name).ToList(),
Contracts = contracts.Select(c => new
{
c.Title, c.Status, c.TotalAmountCents, c.StartDate
}).ToList(),
Reviews = reviews.Select(r => new
{
r.Overall, r.Comment, r.CreatedAt
}).ToList(),
Messages = messages.Select(m => new
{
m.Content, m.CreatedAt
}).ToList()
};
var exportJson = JsonSerializer.Serialize(export, new JsonSerializerOptions
{
WriteIndented = true
});
await _s3.PutObjectAsync(new PutObjectRequest
{
BucketName = _config.ExportsBucket,
Key = $"exports/{userId}/{DateTime.UtcNow:yyyyMMdd}.json",
InputStream = new MemoryStream(Encoding.UTF8.GetBytes(exportJson))
});
return new DataExportResult { Status = "ready", ExpiryDays = 30 };
}
public async Task DeleteUserDataAsync(Guid userId)
{
var user = await _db.Users.FindAsync(userId);
user.Status = "deleted";
user.FullName = "[REDACTED]";
user.Email = $"deleted_{userId}@redacted.invalid";
user.Phone = null;
user.AvatarUrl = null;
user.DeletedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
await _searchIndexer.RemoveFromIndexAsync(userId);
await _redis.KeyDeleteAsync($"user:{userId}:*");
}
}
22. Monitoring & Observability
A marketplace platform requires comprehensive monitoring across three pillars: metrics (quantitative measurements), logs (structured event data), and traces (request lifecycle tracking). We use Datadog for metrics and APM, ELK stack for log aggregation, and Jaeger for distributed tracing. Custom dashboards track marketplace health metrics alongside technical SLOs.
Key Metrics Dashboard
| Category | Metric | Alert Threshold |
|---|---|---|
| Marketplace | Job posting rate (daily) | Below 7-day average by 30% |
| Marketplace | Proposal-to-hire conversion | Below 5% or above 20% |
| Marketplace | Average time to hire (hours) | Above 72 hours |
| Search | Search latency p99 | Above 300ms |
| Search | Search zero-result rate | Above 5% |
| Payments | Payment success rate | Below 99.5% |
| Payments | Escrow funding latency | Above 30 seconds |
| Payments | Payout failure rate | Above 1% |
| Messaging | Message delivery latency p99 | Above 1 second |
| Messaging | WebSocket connection drops | Above 0.1% per hour |
| Reliability | API error rate (5xx) | Above 0.1% |
| Reliability | API latency p99 | Above 500ms |
| Security | Failed login rate | Above 10% per minute |
| Security | API rate limit hits | Above 5% of requests |
Structured Logging
C#public class StructuredLogger
{
public void LogPaymentEvent(string eventType, Guid? userId,
Guid? transactionId, long? amountCents, string status,
string? errorMessage = null)
{
var logEntry = new
{
Timestamp = DateTime.UtcNow,
EventType = eventType,
Service = "payment-service",
UserId = userId?.ToString(),
TransactionId = transactionId?.ToString(),
AmountCents = amountCents,
Status = status,
ErrorMessage = errorMessage,
CorrelationId = Activity.Current?.Id,
Environment = _env.EnvironmentName
};
_logger.LogInformation("{@LogEntry}", logEntry);
}
}
Distributed Tracing
Every request flows through the API Gateway, which generates a correlation ID (W3C Trace Context). This ID is propagated through all service calls, Kafka messages, and background jobs. When a payment fails or a search returns unexpected results, engineers can trace the complete request lifecycle across 15+ services in seconds using the Jaeger UI.
23. Testing Strategy
A freelancing marketplace requires a comprehensive testing strategy spanning unit tests, integration tests, end-to-end tests, load tests, chaos tests, and security tests. Financial subsystems (payments, escrow, invoicing) require the highest test coverage with exhaustive edge case testing. The testing pyramid is: many unit tests at the base, fewer integration tests in the middle, and minimal but critical E2E tests at the top.
Test Coverage Targets
| Component | Coverage Target | Focus Areas |
|---|---|---|
| Payment Service | 95% | Idempotency, currency handling, edge cases |
| Escrow Service | 95% | State machine transitions, auto-release, disputes |
| Contract Service | 90% | Lifecycle transitions, amendments, cancellation |
| Search Service | 90% | Ranking accuracy, index freshness, query parsing |
| Messaging Service | 85% | Delivery guarantees, read receipts, file handling |
| User Service | 90% | Auth flows, OAuth, role management |
| Profile Service | 85% | Completeness scoring, availability calculation |
| Review Service | 90% | Rating computation, fraud detection, timing |
Financial System Tests
C#public class EscrowServiceTests
{
[Fact]
public async Task FundEscrow_WithValidMilestone_CreatesHeldTransaction()
{
// Arrange
var client = await CreateTestClientAsync(balance: 100000);
var freelancer = await CreateTestFreelancerAsync();
var contract = await CreateTestContractAsync(client.Id, freelancer.Id);
var milestone = await CreateTestMilestoneAsync(contract.Id, amountCents: 50000);
// Act
var result = await _escrowService.FundEscrowAsync(milestone.Id, client.Id);
// Assert
Assert.Equal(EscrowStatus.Held, result.Status);
Assert.Equal(50000, result.AmountCents);
var ledger = await GetLedgerEntriesAsync(milestone.Id);
Assert.Equal(2, ledger.Count); // Debit from client, credit to escrow
Assert.Equal("debit", ledger[0].Type);
Assert.Equal("credit", ledger[1].Type);
}
[Fact]
public async Task ReleaseEscrow_ClientApproved_ReleasesToFreelancer()
{
// Arrange
var escrow = await CreateFundedEscrowAsync(amountCents: 50000);
// Act
var result = await _escrowService.ReleaseAsync(
escrow.Id, releasedBy: escrow.ClientId);
// Assert
Assert.Equal(EscrowStatus.Released, result.Status);
Assert.NotNull(result.ReleasedAt);
var payout = await _db.EscrowTransactions.FindAsync(escrow.Id);
Assert.True(payout.PaidOut);
}
[Fact]
public async Task ReleaseEscrow_ClientDidNotApprove_After14Days_AutoReleases()
{
// Arrange
var escrow = await CreateSubmittedEscrowAsync(amountCents: 50000);
escrow.SubmittedForReviewAt = DateTime.UtcNow.AddDays(-15);
await _db.SaveChangesAsync();
// Act
await _autoReleaseService.CheckAndReleaseAsync();
// Assert
var updated = await _db.EscrowTransactions.FindAsync(escrow.Id);
Assert.Equal(EscrowStatus.Released, updated.Status);
Assert.Contains("Auto-release", updated.ReleaseReason);
}
[Fact]
public async Task ReleaseEscrow_InsufficientBalance_ThrowsException()
{
var escrow = await CreateFundedEscrowAsync(amountCents: 50000);
// Simulate platform balance dip
_mockStripe.Setup(s => s.GetBalanceAsync())
.ReturnsAsync(new Balance { Available = 10000 }); // Only $100
await Assert.ThrowsAsync<InsufficientBalanceException>(
() => _escrowService.ReleaseAsync(escrow.Id, escrow.ClientId));
}
[Fact]
public async Task FundEscrow_DuplicateFunding_PreventedByIdempotencyKey()
{
var milestone = await CreateTestMilestoneAsync(amountCents: 50000);
await _escrowService.FundEscrowAsync(milestone.Id, milestone.ClientId);
// Second attempt with same milestone should fail
await Assert.ThrowsAsync<DuplicateFundingException>(
() => _escrowService.FundEscrowAsync(milestone.Id, milestone.ClientId));
}
}
Load Testing
C#// k6 load test configuration for search endpoint
// Run: k6 run --vus 500 --duration 5m search-load-test.js
//
// export default function () {
// const payload = JSON.stringify({
// query: 'React.js developer',
// filters: {
// hourly_rate_min: 30,
// hourly_rate_max: 100,
// availability: 'full_time',
// english_proficiency: 'native'
// },
// sort: 'relevance',
// page: 1
// });
// const params = {
// headers: { 'Content-Type': 'application/json' }
// };
// http.post('https://api.platform.com/api/v1/search/freelancers', payload, params);
// }
//
// Expected results:
// - p50 < 50ms
// - p95 < 100ms
// - p99 < 200ms
// - error_rate < 0.1%
// - throughput > 5000 req/s
Interview Q&A Deep Dive
Core System Design Questions
Q: How would you handle the cold-start problem for a new marketplace?
A: The cold-start problem requires a multi-pronged approach. First, seed one side of the marketplace artificially — invite freelancers from competing platforms with sign-up bonuses. Second, constrain the initial use case: Fiverr started with $5 gigs, Toptal started with Ruby developers. Third, create content marketing that attracts search traffic for specific freelancer types. Fourth, use invite-only access to create scarcity and exclusivity. The key insight is that you don't need to serve all users — you need to create a dense, active marketplace in a narrow niche and expand from there.
Q: How do you ensure payment security and prevent fraud?
A: We implement defense-in-depth: (1) Stripe Connect handles PCI-DSS compliance so we never touch raw card data, (2) every financial transaction is idempotent with unique keys, (3) we maintain a double-entry ledger for audit trails, (4) automated fraud detection flags unusual patterns (sudden large payments, rapid account creation), (5) KYC verification before payouts, (6) escrow holds prevent direct transfers until work is approved, (7) multi-factor authentication for payment actions, and (8) real-time sanctions screening against OFAC/EU lists.
Q: How do you design the matching algorithm?
A: The matching algorithm operates in two layers. The first layer is skill-based filtering and BM25 full-text search via Elasticsearch, which narrows millions of candidates to hundreds. The second layer is a learning-to-rank model that considers 50+ signals: skill match percentage (30%), Job Success Score (20%), rating (15%), earnings history (10%), rate competitiveness (10%), availability (5%), response time (5%), and freshness (5%). These weights are tunable via A/B testing. The NLP component uses sentence embeddings (SBERT) to compute semantic similarity between job descriptions and freelancer bios, catching matches that keyword search misses.
Q: How do you handle disputes fairly?
A: We use a tiered dispute resolution system. Tier 1: Automated resolution for clear-cut cases (no delivery after 14 days = automatic refund, disputed after approval = uphold freelancer). Tier 2: Human mediation for complex cases — both parties submit evidence within 48 hours, a trained mediator reviews the evidence and makes a binding decision. Tier 3: Independent arbitration for high-value disputes ($10K+) involving a third-party arbitrator. The key principle is timeliness: 80% of disputes should resolve within 48 hours. Both parties' trust scores are affected by dispute outcomes to incentivize good-faith behavior.
Technical Deep-Dive Questions
Q: How do you handle search ranking with constantly changing data?
A: We use a dual-index strategy with Kafka CDC (Change Data Capture). PostgreSQL writes trigger Debezium connectors that publish change events to Kafka. The Search Service consumes these events and updates the Elasticsearch index within seconds. For critical fields (rating, JSS, earnings), we also update via synchronous API calls for immediate consistency. The search index is rebuilt nightly from PostgreSQL for eventual consistency. This gives us sub-second freshness for most fields and guaranteed consistency within 24 hours for all fields.
Q: How do you scale the messaging system?
A: Messages are high-volume, low-latency, and require ordering within a conversation. We use a partitioned architecture: conversations are the partition key, ensuring all messages for a conversation go to the same Kafka partition and are processed in order. WebSocket connections are managed by stateless gateway nodes that subscribe to Redis Pub/Sub channels keyed by conversation ID. When a user connects, their gateway node subscribes to all their conversation channels. Message history is stored in PostgreSQL with conversation-level partitioning. We also maintain a hot cache in Redis for the most recent 100 messages per conversation.
Q: How do you prevent freelancers from gaming the review system?
A: Multiple safeguards: (1) Double-blind reviews prevent retaliation, (2) review timing is enforced (14-day window), (3) we detect patterns like too many 5-star reviews from new accounts, (4) low-value contracts ($1) designed to generate fake reviews are automatically flagged, (5) reviews are tied to verified contracts — you can only review after a real contract is completed, (6) our fraud detection ML model analyzes review velocity, rating distribution, reviewer network, and temporal patterns to identify manipulation rings.
Scaling Questions
Q: How would you design for international payments across 180 countries?
A: We use Stripe Connect's global reach, which supports 135+ currencies and 40+ countries natively. For countries not supported by Stripe, we integrate local payment gateways (PayPal for US, Payoneer for CIS countries, bank transfer for others). The currency conversion happens at the escrow funding rate, protecting freelancers from exchange rate volatility. Tax compliance is handled per-country: W-9/W-8BEN for US, VAT numbers for EU, TDS for India. We partner with a tax preparation service (Tax1099 or similar) for automated document generation. Payout frequency varies by country: daily for US/EU, weekly for most others, monthly for high-risk jurisdictions.
Q: How do you handle platform scaling from 1M to 50M users?
A: The architecture scales horizontally at every layer. Application servers are stateless and run behind an auto-scaling group. The database uses read replicas for read-heavy operations (search, profile views) and connection pooling (PgBouncer) for write operations. Elasticsearch scales by adding data nodes. Redis scales via clustering with hash-slot distribution. Kafka scales by adding partitions. The key bottleneck at scale is the search index — we implement index sharding by skill category, hot/cold index separation, and aggressive caching of popular searches. At 50M users, we'd also consider moving to a dedicated data warehouse (Snowflake/BigQuery) for analytics and ML model training, keeping the operational database focused on OLTP workloads.
Pre-Interview Checklist
- Understand the two-sided marketplace dynamics and cold-start problem
- Know escrow payment flows and the state machine for fund lifecycle
- Design a skill taxonomy with hierarchical relationships and synonyms
- Explain the matching algorithm and how to A/B test ranking changes
- Know Stripe Connect integration patterns and idempotency requirements
- Discuss dispute resolution workflows with evidence-based decision-making
- Understand two-sided review systems and anti-fraud measures
- Explain the contract lifecycle state machine and amendment process
- Know how to handle time tracking integrity and screenshot privacy
- Discuss GDPR data export/deletion requirements and implementation
- Understand search index freshness tradeoffs (CDC vs synchronous)
- Explain international payment challenges (currency, tax, compliance)
- Know the security posture for financial systems (PCI-DSS, encryption, audit)
- Discuss monitoring SLOs and error budgets for critical payment paths