We develop custom loyalty systems for online stores, marketplaces, and services. Recently, we implemented a solution that increased average order value by 25% and boosted user retention by 40% in the first months. A typical scenario: a client uses a ready-made plugin but cannot configure earning multipliers for premium categories or implement point expiration using FIFO. Moreover, standard plugins often generate N+1 queries when checking balance, causing performance drops under high load. We use Laravel 11, PostgreSQL, and Redis to ensure responsiveness even at 1000 requests per second. For example, one of our clients—an electronics e-commerce store—faced order checkout slowdown due to N+1 queries to the loyalty system. After implementing a custom solution with caching and batch inserts, order processing time dropped from 2 seconds to 200 ms. Our custom loyalty system is 2 times more effective at boosting repeat purchases than off-the-shelf plugins. Investment starts at $4,900 for a basic system, and our clients typically see a 25% increase in average order value, paying back in 4–6 months. In this article, we break down the architecture, common issues, and our implementation approach.
What problems do we solve
Most ready-made solutions do not account for business specifics. Typical pain points:
- N+1 queries when accruing points—each operation hits the DB separately. We solve this with batch inserts and balance caching in Redis.
- Concurrent redemption—race conditions when two orders spend the same points. We use
SELECT ... FOR UPDATEand transactions. - Complex campaign rules—category multipliers, minimum cart gifts, time-limited promotions. We implement via configurable tables with JSONB conditions.
- Point expiration—FIFO calculation and partial spending handling.
How it works: core architecture
The central element is the transaction log (Wikipedia). The balance is always derived from the sequence of operations, ensuring auditability.
-- Bonus account per user
CREATE TABLE loyalty_accounts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT UNIQUE REFERENCES users(id),
balance DECIMAL(12,2) DEFAULT 0,
lifetime_earned DECIMAL(12,2) DEFAULT 0,
tier_id BIGINT REFERENCES loyalty_tiers(id),
expires_at DATE,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- All point movements (append-only log)
CREATE TABLE loyalty_transactions (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT REFERENCES loyalty_accounts(id),
type VARCHAR(32) NOT NULL, -- 'earn', 'redeem', 'expire', 'adjust', 'refund'
amount DECIMAL(12,2) NOT NULL,
balance_after DECIMAL(12,2) NOT NULL,
reason VARCHAR(255),
source_type VARCHAR(64), -- 'order', 'manual', 'birthday', 'referral'
source_id BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Program tiers
CREATE TABLE loyalty_tiers (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL, -- Bronze, Silver, Gold, Platinum
min_lifetime DECIMAL(12,2) NOT NULL,
earn_multiplier DECIMAL(4,2) DEFAULT 1.0,
redeem_rate DECIMAL(4,2) DEFAULT 1.0,
perks JSONB
);
The transaction log is a fundamental architectural choice. The balance is either calculated from history or stored denormalized and recalculated on discrepancy. This allows auditing any movement.
How to avoid point loss during concurrent redemptions?
The main issue—two simultaneous orders can spend the same points. The solution—account row locking and atomic transactions.
class LoyaltyService {
public function earnPoints(User $user, float $amount, string $sourceType, int $sourceId): LoyaltyTransaction {
$account = LoyaltyAccount::firstOrCreate(['user_id' => $user->id]);
$tier = $account->tier ?? LoyaltyTier::where('min_lifetime', 0)->orderBy('min_lifetime')->first();
$points = round($amount * $tier->earn_multiplier * config('loyalty.earn_rate'));
return DB::transaction(function() use ($account, $points, $sourceType, $sourceId) {
$newBalance = $account->balance + $points;
$account->update([
'balance' => $newBalance,
'lifetime_earned' => $account->lifetime_earned + $points,
]);
$newTier = LoyaltyTier::where('min_lifetime', '<=', $account->lifetime_earned)
->orderByDesc('min_lifetime')
->first();
if ($newTier && $newTier->id !== $account->tier_id) {
$account->update(['tier_id' => $newTier->id]);
event(new TierUpgraded($account->user, $newTier));
}
return LoyaltyTransaction::create([
'account_id' => $account->id,
'type' => 'earn',
'amount' => $points,
'balance_after'=> $newBalance,
'source_type' => $sourceType,
'source_id' => $sourceId,
'reason' => 'Accrual for purchase',
]);
});
}
}
Why choose a custom loyalty system over ready-made solutions?
Ready-made plugins often lack flexibility in accrual rules, CRM integration, and analytics. Compare:
| Criteria | Ready-made solution | Custom development |
|---|---|---|
| Campaign configuration | Limited template set | Any logic with arbitrary conditions |
| Integration | Only standard CMS | Via API with any systems (1C, ERP, CRM) |
| Scalability | Depends on platform | Optimized for load (Redis, queues) |
| Analytics | Only basic reports | Custom dashboards and segmentation |
A custom system pays for itself in 4–6 months through increased average check (20–30%) and 1.5x LTV growth. Our custom loyalty system is 2x better at boosting repeat purchases compared to off-the-shelf plugins. We specialize in e-commerce loyalty system development with points, tiers, and campaigns.
What loyalty tiers can be configured?
A typical tier hierarchy—from Bronze to Platinum—with different earning multipliers and privileges. For example:
| Tier | Minimum lifetime spend | Earning multiplier | Privileges |
|---|---|---|---|
| Bronze | $0 | 1.0 | Basic program |
| Silver | $100 | 1.2 | Priority support |
| Gold | $500 | 1.5 | Free shipping |
| Platinum | $1000 | 2.0 | Personal manager |
Conditions can be customized for any business.
Implementation process
- Analysis: study business processes, gather requirements, prepare prototype.
- Design: database architecture, API, UI widgets. Align campaign logic.
- Development: iterative delivery every 2–3 days.
- Testing: unit and integration tests, load testing (up to 1000 RPS).
- Deployment: configure CI/CD, monitoring (Sentry, Grafana), documentation.
What's included
- Architecture and documentation: DB schema description, API (OpenAPI), admin instructions.
- Source code: backend + frontend, test coverage at least 80%.
- Access: dedicated repository, development and staging environments.
- Support: 1 month post-production support, then by SLA.
Estimated timelines and investment
Basic version with earning, spending, and transaction history—1.5–2 weeks, starting at $4,900. Extended version with tiers, campaigns, and point expiration—3–4 weeks, from $9,500. Mobile loyalty card with QR code and POS integration—adds 2–3 weeks, from $3,000. With over 10 years of e-commerce experience and 50+ successful loyalty system projects (including certified Laravel and React developers), we deliver guaranteed performance: tested up to 1000 RPS with a 12-month warranty on code and free bug fixes. Our custom loyalty system development services include points, tiers, and campaigns. Contact us to evaluate your project—we will assess within one day. Get a consultation on architecture and timelines. Our team has 10+ years of experience in e-commerce and has delivered 50+ loyalty systems.







