Skip to main content

2 posts tagged with "nodejs"

View All Tags

Managing Concurrent Financial Operations Over WebSockets with RBAC

· 5 min read
Backend Engineering
Backend Systems & Architecture

Building real-time applications that handle financial transactions requires careful orchestration of user permissions, concurrent operations, and budget management. In this post, we'll explore architectural patterns for implementing secure, concurrent financial operations over WebSocket connections while respecting role-based access control.

The Engineering Challenge

When users perform real-time operations that consume resources or credits, you need a system that can:

  • Process operations concurrently while maintaining data consistency
  • Enforce permissions before allowing resource consumption
  • Support hierarchical budgets (organization → project → user)
  • Handle race conditions without double-spending
  • Provide real-time feedback to connected clients

High-Level Architecture

A robust solution combines WebSocket messaging with transactional databases and permission systems:

graph TD
A[Client Operation] --> B[WebSocket Handler]
B --> C[Permission Validation]
C --> D[Resource Allocation Logic]
D --> E[Budget Hierarchy Check]
E --> F[Fallback Budget Source]
F --> G[Atomic Transaction]
G --> H[Real-time Update Broadcast]
H --> I[Client Acknowledgment]

Permission-Based Resource Management

Before any resource consumption, validate user permissions within the operational context:

async function validateAndProcessOperation(connection, resourceAmount, operationDetails, idempotencyKey) {
const permissionSystem = require('./permissions');
const contextId = connection.sessionContext?.contextId || null;
const organizationId = connection.sessionContext?.organizationId || null;

// Verify user has operational permissions in this context
const hasPermission = await permissionSystem.checkPermission(
connection.user.id,
'resource:consume',
organizationId,
contextId
);

if (!hasPermission) {
connection.send(JSON.stringify({
type: 'error',
message: 'Permission denied: Insufficient privileges for this operation'
}));
return { success: false, error: 'Permission denied' };
}

// Proceed with resource allocation...
}

Hierarchical Budget Management

Implement a cascading budget system where resources flow from organization → project → individual operations:

// Smart resource allocation with fallback hierarchy
if (contextId) {
const contextBudget = await budgetService.getContextBudget(contextId);

if (contextBudget >= resourceAmount) {
console.log(`Using context budget (${contextBudget} available)`);

const result = await budgetService.deductFromContext(
contextId,
resourceAmount,
connection.user.id,
operationDetails,
idempotencyKey,
onSuccessCallback
);

return {
...result,
source: 'context_budget',
contextId: contextId
};
} else {
console.log(`Context budget insufficient, falling back to organization pool`);
}
}

// Fall back to organization-level resource pool
const result = await budgetService.deductFromOrganization(
organizationId,
resourceAmount,
connection.user.id,
operationDetails,
idempotencyKey
);

Concurrency Control and Race Prevention

Prevent double-spending and maintain consistency using database-level controls:

async function performAtomicResourceDeduction(contextId, amount, userId, details, idempotencyKey, callback) {
const client = await this.pool.connect();
try {
await client.query('BEGIN');

// Prevent duplicate operations using idempotency
if (idempotencyKey) {
const duplicateCheck = await client.query(
'SELECT id FROM operation_ledger WHERE idempotency_key = $1',
[idempotencyKey]
);

if (duplicateCheck.rows.length > 0) {
await client.query('ROLLBACK');
return { success: true, isDuplicate: true };
}
}

// Acquire exclusive lock on budget row
const budgetResult = await client.query(
'SELECT available_budget FROM contexts WHERE id = $1 FOR UPDATE',
[contextId]
);

const currentBudget = budgetResult.rows[0].available_budget;
if (currentBudget < amount) {
await client.query('ROLLBACK');
return { success: false, error: 'Insufficient budget' };
}

// Atomically update budget and create audit trail
await client.query(
'UPDATE contexts SET available_budget = available_budget - $1 WHERE id = $2',
[amount, contextId]
);

await client.query(
'INSERT INTO operation_ledger (context_id, user_id, amount, operation_type, details, idempotency_key) VALUES ($1, $2, $3, $4, $5, $6)',
[contextId, userId, -amount, 'resource_consumption', details.description, idempotencyKey]
);

await client.query('COMMIT');

// Execute post-commit actions
if (callback) {
await callback({
newBudget: currentBudget - amount,
amountProcessed: amount,
source: 'context_budget'
});
}

return { success: true, newBudget: currentBudget - amount };

} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}

Real-time State Synchronization

After successful operations, broadcast updates to maintain consistency across all client sessions:

const onSuccessCallback = async (result) => {
// Update current WebSocket connection
if (connection.readyState === WebSocket.OPEN) {
if (result.source === 'context_budget') {
connection.send(JSON.stringify({
type: 'budget_update',
contextId: contextId,
newBudget: result.newBudget,
amountProcessed: result.amountProcessed
}));
} else {
connection.send(JSON.stringify({
type: 'organization_budget_update',
newBalance: result.newBalance,
amountProcessed: result.amountProcessed
}));
}
}

// Broadcast to all sessions for this user
broadcastToUserSessions(connection.user.id, {
type: 'budget_sync',
newBalance: result.newBalance
});
};

Performance Optimizations

Several patterns ensure optimal performance under load:

  1. Connection Pooling for database connections
  2. Permission Caching with time-based invalidation
  3. Batch Processing for multiple concurrent operations
  4. Optimistic Locking to reduce contention
// Cached permission checking
const permissionCache = new Map();
const PERMISSION_CACHE_TTL = 300000; // 5 minutes

async function getCachedPermission(userId, permission, organizationId, contextId) {
const cacheKey = `${userId}:${permission}:${organizationId}:${contextId}`;
const cached = permissionCache.get(cacheKey);

if (cached && (Date.now() - cached.timestamp) < PERMISSION_CACHE_TTL) {
return cached.hasPermission;
}

const hasPermission = await permissionSystem.checkPermission(
userId, permission, organizationId, contextId
);

permissionCache.set(cacheKey, {
hasPermission,
timestamp: Date.now()
});

return hasPermission;
}

Testing Concurrent Operations

Comprehensive testing ensures system reliability under concurrent load:

// Concurrent operation stress test
describe('Concurrent Resource Operations', () => {
it('should handle multiple simultaneous operations without conflicts', async () => {
const operations = [];

// Simulate 10 concurrent resource consumption requests
for (let i = 0; i < 10; i++) {
operations.push(
budgetService.deductFromContext(
contextId,
10,
userId,
{ description: `Operation ${i}` },
`test-${i}-${Date.now()}`
)
);
}

const results = await Promise.all(operations);

// Verify all operations succeeded and budget is consistent
const successfulOps = results.filter(r => r.success);
expect(successfulOps.length).toBe(10);

const finalBudget = await budgetService.getContextBudget(contextId);
expect(finalBudget).toBe(initialBudget - 100);
});
});

Architectural Principles

Key lessons for building concurrent financial systems:

  1. Atomic Operations - Use database transactions for financial consistency
  2. Idempotency - Prevent duplicate operations in distributed systems
  3. Permission Caching - Balance security with performance requirements
  4. Callback Patterns - Decouple transaction logic from side effects
  5. Connection Management - Pool connections for WebSocket scalability

This architecture successfully handles thousands of concurrent operations while maintaining financial accuracy and respecting user permissions. The foundation is combining ACID database properties with real-time WebSocket communication and robust permission systems.


Explore more architectural patterns in our posts on subscription-based feature gating and system resilience.

Implementing Dynamic Feature Gating for Multi-Tier SaaS Applications

· 6 min read
Backend Engineering
Backend Systems & Architecture

When building SaaS platforms with multiple subscription tiers, controlling feature access based on billing plans becomes a fundamental architectural concern. In this post, we'll explore patterns for implementing flexible, scalable feature gating systems that integrate seamlessly with payment providers and adapt to evolving business models.

The Architecture Challenge

Modern SaaS applications typically offer tiered pricing with progressive feature unlocks:

  • Basic Tier ($X/month) - Core functionality and basic features
  • Professional Tier ($Y/month) - Enhanced features and integrations
  • Enterprise Tier ($Z/month) - Advanced capabilities and customization

The system must:

  • Control feature visibility based on subscription status
  • Integrate with payment systems for real-time plan detection
  • Cache decisions efficiently for performance at scale
  • Provide upgrade guidance when users hit feature limits
  • Support gradual rollouts and A/B testing scenarios

High-Level System Design

An effective solution combines subscription detection, permission caching, and feature enforcement:

graph TD
A[User Request] --> B[Subscription Detection]
B --> C[Payment Provider Query]
C --> D[Feature Permission Cache]
D --> E[Feature Gate Evaluation]
E --> F[Access Granted/Denied]
F --> G[Upgrade Recommendation Engine]

Subscription Detection Service

The foundation is a service that maps payment provider data to internal feature permissions:

class SubscriptionManager {
constructor() {
// Map payment provider plan IDs to internal tiers
this.PLAN_TIER_MAPPING = {
'plan_basic_monthly': 'basic',
'plan_professional_monthly': 'professional',
'plan_enterprise_monthly': 'enterprise'
};

// Define tier hierarchy for upgrade paths
this.TIER_HIERARCHY = {
'basic': 1,
'professional': 2,
'enterprise': 3
};

// Features available per tier
this.TIER_FEATURES = {
'basic': [
'core:functionality',
'basic:integrations',
'standard:support'
],
'professional': [
'core:functionality',
'basic:integrations',
'standard:support',
'advanced:analytics',
'premium:integrations',
'custom:branding'
],
'enterprise': [
'core:functionality',
'basic:integrations',
'standard:support',
'advanced:analytics',
'premium:integrations',
'custom:branding',
'enterprise:sso',
'priority:support',
'white:label'
]
};
}
}

Real-time Subscription Status

Query payment providers efficiently while maintaining performance through caching:

async function getUserSubscriptionTier(userId, organizationId) {
// Check memory cache first
const cacheKey = `${userId}-${organizationId}`;
const cached = this.cache.get(cacheKey);

if (cached && (Date.now() - cached.timestamp) < this.CACHE_DURATION) {
return cached.tier;
}

try {
// Query recent subscription activity
const client = await database.pool.connect();

const result = await client.query(`
SELECT se.plan_id, se.created_at
FROM subscription_events se
WHERE se.organization_id = $1
AND se.event_type = 'payment_succeeded'
AND se.plan_id IS NOT NULL
AND se.created_at > NOW() - INTERVAL '45 days'
ORDER BY se.created_at DESC
LIMIT 1
`, [organizationId]);

client.release();

if (result.rows.length === 0) {
return null; // No active subscription
}

const planId = result.rows[0].plan_id;
const subscriptionTier = this.PLAN_TIER_MAPPING[planId];

// Update cache
this.cache.set(cacheKey, {
tier: subscriptionTier,
timestamp: Date.now()
});

return subscriptionTier;

} catch (error) {
console.error(`Error determining subscription tier for user ${userId}:`, error);
return null;
}
}

Feature Gate Middleware

Implement middleware that automatically enforces subscription-based access control:

const featureGate = (requiredFeature) => {
return async (req, res, next) => {
try {
const { user } = req;
const organizationId = req.organizationId;

// Check feature access
const hasAccess = await subscriptionManager.hasFeatureAccess(
user.id,
organizationId,
requiredFeature
);

if (!hasAccess) {
const upgradeInfo = await subscriptionManager.getUpgradeRecommendation(
user.id,
organizationId,
requiredFeature
);

return res.status(403).json({
error: 'Feature requires subscription upgrade',
requiredFeature: requiredFeature,
upgrade: upgradeInfo
});
}

next();
} catch (error) {
console.error('Feature gate evaluation error:', error);
res.status(500).json({ error: 'Access control system unavailable' });
}
};
};

// Usage in API routes
router.post('/advanced-analytics',
authenticateUser,
featureGate('advanced:analytics'),
generateAnalyticsReport
);

router.post('/premium-integration/setup',
authenticateUser,
featureGate('premium:integrations'),
configurePremiumIntegration
);

Intelligent Upgrade Recommendations

When users encounter feature limits, provide contextual upgrade guidance:

async function getUpgradeRecommendation(userId, organizationId, requestedFeature) {
const currentTier = await this.getUserSubscriptionTier(userId, organizationId);
const currentLevel = currentTier ? this.TIER_HIERARCHY[currentTier] : 0;

// Find minimum tier required for requested feature
let requiredTier = null;
for (const [tier, features] of Object.entries(this.TIER_FEATURES)) {
if (features.includes(requestedFeature)) {
const tierLevel = this.TIER_HIERARCHY[tier];
if (tierLevel > currentLevel) {
requiredTier = tier;
break;
}
}
}

if (!requiredTier) {
return {
needsUpgrade: false,
message: 'Feature not available in any subscription tier'
};
}

const tierDisplayNames = {
'basic': 'Basic Plan ($X/month)',
'professional': 'Professional Plan ($Y/month)',
'enterprise': 'Enterprise Plan ($Z/month)'
};

return {
needsUpgrade: true,
currentTier: currentTier,
requiredTier: requiredTier,
requiredTierName: tierDisplayNames[requiredTier],
message: `This feature requires ${tierDisplayNames[requiredTier]} or higher`,
upgradeUrl: `/billing/upgrade?plan=${requiredTier}`
};
}

Client-Side Feature Control

Dynamically show/hide features based on subscription status:

// Load subscription permissions on page load
async function loadSubscriptionPermissions() {
try {
const response = await fetch('/api/subscription/permissions');
const permissions = await response.json();

// Control UI visibility based on subscription tier
if (permissions.hasAdvancedAnalytics) {
document.getElementById('analytics-section').style.display = 'block';
} else {
document.getElementById('analytics-section').style.display = 'none';
document.getElementById('analytics-upgrade-prompt').style.display = 'block';
}

if (permissions.hasPremiumIntegrations) {
document.getElementById('integrations-section').style.display = 'block';
} else {
document.getElementById('integrations-section').style.display = 'none';
document.getElementById('integrations-upgrade-prompt').style.display = 'block';
}

// Store permissions for runtime checks
window.subscriptionPermissions = permissions;

} catch (error) {
console.error('Failed to load subscription permissions:', error);
}
}

// Check permissions before expensive operations
async function performAdvancedOperation() {
if (!window.subscriptionPermissions?.hasAdvancedAnalytics) {
showUpgradeModal('Advanced Analytics', 'professional');
return;
}

// Proceed with feature...
}

Performance Optimization Strategies

Ensure the system scales efficiently under production load:

Multi-Layer Caching

// In-memory cache for immediate access
this.memoryCache = new Map();

// Database cache for persistence across server restarts
await client.query(`
INSERT INTO subscription_cache
(user_id, organization_id, subscription_tier, features, expires_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (user_id, organization_id)
DO UPDATE SET subscription_tier = EXCLUDED.subscription_tier
`, [userId, organizationId, tier, JSON.stringify(features), expiresAt]);

Batch Permission Evaluation

// Evaluate multiple features simultaneously
const permissions = await subscriptionManager.getBulkPermissions(userId, organizationId);
const hasAnalytics = permissions.features.includes('advanced:analytics');
const hasIntegrations = permissions.features.includes('premium:integrations');

Lazy Permission Loading

// Only check permissions when features are accessed
const checkFeatureAccess = memoize(async (userId, organizationId, feature) => {
return await subscriptionManager.hasFeatureAccess(userId, organizationId, feature);
});

Testing Feature Gates

Comprehensive testing ensures correct behavior across subscription tiers:

describe('Subscription Feature Gates', () => {
it('should grant access to tier-appropriate features', async () => {
// Mock user with Professional subscription
const mockUser = await createMockUser('professional');

const hasAnalyticsAccess = await subscriptionManager.hasFeatureAccess(
mockUser.id,
mockUser.organizationId,
'advanced:analytics'
);

expect(hasAnalyticsAccess).toBe(true);

const hasEnterpriseAccess = await subscriptionManager.hasFeatureAccess(
mockUser.id,
mockUser.organizationId,
'enterprise:sso'
);

expect(hasEnterpriseAccess).toBe(false);
});

it('should provide accurate upgrade recommendations', async () => {
const mockUser = await createMockUser('basic');

const upgrade = await subscriptionManager.getUpgradeRecommendation(
mockUser.id,
mockUser.organizationId,
'advanced:analytics'
);

expect(upgrade.needsUpgrade).toBe(true);
expect(upgrade.requiredTier).toBe('professional');
expect(upgrade.upgradeUrl).toContain('plan=professional');
});
});

Analytics and Business Intelligence

Track feature usage patterns to inform product and pricing decisions:

// Log feature access attempts for analysis
await client.query(`
INSERT INTO feature_usage_analytics (user_id, organization_id, feature, subscription_tier, access_granted)
VALUES ($1, $2, $3, $4, $5)
`, [userId, organizationId, feature, currentTier, hasAccess]);

// Track subscription upgrade conversions
await client.query(`
INSERT INTO upgrade_conversion_events (user_id, organization_id, from_tier, to_tier, trigger_feature)
VALUES ($1, $2, $3, $4, $5)
`, [userId, organizationId, oldTier, newTier, triggerFeature]);

Key Design Principles

Essential patterns for building scalable subscription systems:

  1. Cache Aggressively - Feature checks happen on every request
  2. Fail Gracefully - Show upgrade prompts instead of hard blocks
  3. Test Extensively - Cover subscription transitions and edge cases
  4. Monitor Usage - Data drives feature and pricing optimization
  5. Keep Logic Simple - Complex rules create maintenance overhead

This architecture has proven effective for managing feature access across thousands of subscribers while maintaining performance and providing clear monetization paths. The foundation is building flexibility and observability into the system from the beginning.


Discover more SaaS architecture patterns in our posts on concurrent financial operations and system resilience.