Referral System Development
Referral program is mechanism where existing user gets reward for attracting new one. In practice, this is set of technical tasks: unique link generation, click attribution, condition fulfillment tracking, reward accrual and payment.
Data Model
CREATE TABLE referral_codes (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id),
code VARCHAR(32) UNIQUE NOT NULL,
type VARCHAR(32) DEFAULT 'personal', -- personal/promo/partner
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE referral_clicks (
id BIGSERIAL PRIMARY KEY,
code_id BIGINT REFERENCES referral_codes(id),
ip INET,
user_agent TEXT,
landed_at TIMESTAMPTZ DEFAULT NOW(),
converted BOOLEAN DEFAULT FALSE
);
CREATE TABLE referrals (
id BIGSERIAL PRIMARY KEY,
referrer_id BIGINT REFERENCES users(id), -- who brought
referred_id BIGINT REFERENCES users(id), -- who was brought
code_id BIGINT REFERENCES referral_codes(id),
status VARCHAR(32) DEFAULT 'pending', -- pending/qualified/rewarded/cancelled
qualified_at TIMESTAMPTZ, -- condition fulfilled
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE referral_rewards (
id BIGSERIAL PRIMARY KEY,
referral_id BIGINT REFERENCES referrals(id),
recipient_id BIGINT REFERENCES users(id), -- referrer or referred (two-way programs)
type VARCHAR(32), -- 'cashback', 'bonus_points', 'discount'
amount DECIMAL(14,2),
currency CHAR(3) DEFAULT 'RUB',
status VARCHAR(32) DEFAULT 'pending', -- pending/paid/cancelled
paid_at TIMESTAMPTZ
);
Unique Code Generation
class ReferralCodeService {
public function generateForUser(User $user): ReferralCode {
// Check if code already exists
if ($existing = ReferralCode::where('user_id', $user->id)->first()) {
return $existing;
}
do {
// Readable code based on username + random suffix
$base = strtoupper(substr(preg_replace('/[^a-z]/i', '', $user->name), 0, 4));
$code = $base . strtoupper(Str::random(4));
} while (ReferralCode::where('code', $code)->exists());
return ReferralCode::create([
'user_id' => $user->id,
'code' => $code,
]);
}
}
Referral Link and Cookie
Referral parameter passed via URL: https://example.com/register?ref=IVAN4X2K. Must save attribution even if user doesn't register immediately:
// Middleware: ReferralTracker
class ReferralTrackerMiddleware {
public function handle(Request $request, Closure $next): Response {
$code = $request->query('ref');
if ($code && !session()->has('referral_code')) {
$referralCode = ReferralCode::where('code', $code)->first();
if ($referralCode) {
session(['referral_code' => $code]);
// Log click
ReferralClick::create([
'code_id' => $referralCode->id,
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
}
}
return $next($request);
}
}
Attribution on Registration
// In UserRegistrationService
public function register(array $data): User {
$user = User::create($data);
$referralCode = session()->pull('referral_code');
if ($referralCode) {
$code = ReferralCode::where('code', $referralCode)->first();
if ($code && $code->user_id !== $user->id) {
Referral::create([
'referrer_id' => $code->user_id,
'referred_id' => $user->id,
'code_id' => $code->id,
'status' => 'pending',
]);
// Mark click as converted
ReferralClick::where('code_id', $code->id)
->where('converted', false)
->latest('landed_at')
->first()
?->update(['converted' => true]);
}
}
return $user;
}
Reward Accrual Conditions
Referral is "qualified" only when specific condition met: first payment, purchase threshold reached, trial period ended. Implemented via Events:
// Event: first payment of new user
class FirstPurchaseMade {
public function __construct(public Order $order) {}
}
// Listener
class QualifyReferralOnFirstPurchase {
public function handle(FirstPurchaseMade $event): void {
$referral = Referral::where('referred_id', $event->order->user_id)
->where('status', 'pending')
->first();
if (!$referral) return;
DB::transaction(function() use ($referral, $event) {
$referral->update([
'status' => 'qualified',
'qualified_at' => now(),
]);
$program = ReferralProgram::active()->first();
// Reward to referrer
ReferralReward::create([
'referral_id' => $referral->id,
'recipient_id' => $referral->referrer_id,
'type' => $program->reward_type,
'amount' => $this->calculateReward($program, $event->order),
'status' => 'pending',
]);
// Two-way program: bonus to new user too
if ($program->reward_referred) {
ReferralReward::create([
'referral_id' => $referral->id,
'recipient_id' => $referral->referred_id,
'type' => $program->referred_reward_type,
'amount' => $program->referred_reward_amount,
'status' => 'pending',
]);
}
});
}
private function calculateReward(ReferralProgram $program, Order $order): float {
return match($program->reward_type) {
'fixed' => $program->reward_amount,
'percentage' => round($order->total * $program->reward_percent / 100, 2),
default => 0,
};
}
}
Reward Payout
Accrued rewards paid in batch (weekly) or immediately — depends on type:
// Bonus points — immediately
class CreditBonusPoints implements ShouldQueue {
public function handle(ReferralRewardCreated $event): void {
$reward = $event->reward;
if ($reward->type !== 'bonus_points') return;
BonusAccount::firstOrCreate(['user_id' => $reward->recipient_id])
->increment('balance', $reward->amount);
$reward->update(['status' => 'paid', 'paid_at' => now()]);
}
}
Referrer Personal Account
Referral program statistics page:
interface ReferralStats {
code: string;
link: string;
clicks_total: number;
registered: number;
qualified: number;
earned_total: number;
pending_amount: number;
}
Fraud Protection
Basic checks:
// Self-referral (user entered own code)
if ($referral->referrer_id === $event->order->user_id) {
$referral->update(['status' => 'cancelled']);
return;
}
// One IP registered multiple accounts
$sameIpUsers = DB::table('user_registrations')
->where('ip', $event->order->user->registration_ip)
->where('created_at', '>', now()->subDays(7))
->count();
if ($sameIpUsers > 3) {
$referral->update(['status' => 'fraud_suspect']);
return;
}
Implementation Timeline
Basic referral system with codes, attribution, and fixed reward accrual: 1–1.5 weeks. Two-way program with percentage bonuses, personal account, fraud protection: 2–2.5 weeks. Multi-level (MLM-like) referral system with referral tree: plus 1–2 weeks.







