A Practical Guide with Angular Implementation and Production Best Practices
Modern enterprise applications often deal with shared resources: documents, customer records, workflow tasks, configuration pages, or business approval screens. When multiple users try to modify the same resource at the same time, it creates the risk of data overwrites, partial updates, race conditions, and inconsistent business states.
This is where distributed locks become useful. But it is not enough to simply take a lock on the server. In real-world applications, users need clear, real-time feedback in the UI. They should instantly know:
Who is editing a record
Whether the record is locked or available
When the lock is released
When another user takes the lock
How long a lock will last
Whether lock renewal or override is allowed
A well-designed real-time distributed lock indication UI significantly improves collaboration, reduces conflicts, and enhances productivity.
This article explains how to build such a system with focus on:
Principles of distributed locking
Real-time communication channels
Server-side design considerations
A production-grade Angular implementation
Failover, stale lock handling, performance, and scalability
Testing, monitoring, and operational practices
1. Why Distributed Lock Indication Matters
1.1. The classic problem
Imagine a CRM system where two users open the same customer record. If both make changes, the last save wins. The earlier user loses data without any warning. This leads to:
1.2. The business expectation
Users expect productivity tools to be collaborative by default. They want the system to prevent conflicts, help them know who is doing what, and show clear signals when a record is being edited.
1.3. Lock indication enables collaboration
A distributed lock indication UI:
shows instantly when someone else starts editing
disables risky UI actions
provides context (who is editing and from where)
synchronises multiple browser windows
helps auditing and documentation
2. Types of Distributed Locks
Before we design the UI, we must understand the underlying locking patterns.
2.1. Pessimistic Lock
The system blocks others immediately.
Only one user gets the lock
Others must wait
Ensures strict consistency
Common for workflow-heavy systems
2.2. Optimistic Lock
The system allows concurrent reads, checks conflicts during save.
No indication till save attempt
Less strict but more scalable
Common in high throughput APIs
2.3. Soft Lock with UI Indication
A hybrid approach:
Locks are advisory, not blocking
UI warns users but does not prevent reading
Saves can be handled with version checks
Humans can coordinate and avoid conflicts
This article focuses on pessimistic or soft locks with real-time UI indication, because that is where the UI complexity arises.
3. Real-Time Lock Indication Architecture
Distributed locking requires coordination between clients and the server.
3.1. Key components
Lock Manager (API layer or distributed cache)
Pub/Sub or event distribution system
WebSocket or SSE layer for real-time events
Angular components and services
State management system (NGXS, NGRX, or RxJS service)
3.2. Example workflow
User A opens record 101.
User A requests a lock.
Server grants lock and publishes event:
record-101 locked by user-A.
User B opens record 101.
User B receives event and UI shows:
“Record is being edited by user-A.”
When user A closes browser or navigates away:
Server releases lock and publishes event.
User B sees UI update instantly.
3.3. Event-like interactions
Events to broadcast:
lock_acquired
lock_denied
lock_released
lock_expired
lock_renewed
lock_override
4. Selecting Technology for Real-Time Updates
4.1. WebSockets
Good for bidirectional communication.
Useful when clients also send lock renewal pings.
4.2. Server-Sent Events (SSE)
One-directional, simpler, less overhead.
Good for pure indication.
4.3. SignalR, Socket.io, WebPubSub services (cloud)
Managed services that reduce complexity.
4.4. Redis Pub/Sub or Kafka
Server-to-server event broadcast layer.
Your API consumes events and pushes to WebSockets.
For enterprise Angular apps, a common architecture is:
Client (Angular) <-> WebSocket Gateway <-> Redis Pub/Sub <-> Lock Manager
5. Designing a Robust Lock Manager (Server Side)
5.1. Lock storage
Use a distributed, highly available cache like:
Redis
Hazelcast
Apache Ignite
Aerospike
Store lock as:
lock:record:101 = {
userId: "u123",
userName: "Amit Sharma",
expiresAt: 1738582050000,
sessionId: "browser-tab-session-id"
}
5.2. Lock TTL
Always enforce a TTL to avoid stale locks.
Typical TTL: 2 to 10 minutes.
Clients must renew periodically.
5.3. Lock acquisition rules
If lock not present, grant.
If lock exists and user is same (same tab), renew.
If lock exists but expired, override.
If lock exists and active, return lock info and deny.
5.4. Lock release
Triggered by:
5.5. Publishing lock events
On lock changes, publish:
publish("lock-events", {
recordId: 101,
type: "lock_acquired",
by: "u123"
})
6. Angular Architecture for Real-Time Lock Indication
We will break the UI implementation into:
Lock service
WebSocket service
A lock state store
Angular resolvers and guards
UI component integration
Cleanup and stability handling
6.1. Angular Folder Structure
/src/app/shared/lock/
lock.service.ts
lock-events.service.ts
lock.state.ts
lock.model.ts
/src/app/features/customer/edit/
edit-customer.component.ts
edit-customer.resolver.ts
edit-customer.guard.ts
7. Step-by-Step Angular Implementation
7.1. Lock Model
export interface LockInfo {
recordId: string;
lockedBy: string;
lockedByName: string;
lockedAt: number;
expiresAt: number;
isOwnedByMe: boolean;
}
7.2. Lock REST API Usage
@Injectable({ providedIn: 'root' })
export class LockService {
constructor(private http: HttpClient) {}
acquireLock(recordId: string) {
return this.http.post<LockInfo>(
`/api/lock/acquire`,
{ recordId }
);
}
releaseLock(recordId: string) {
return this.http.post(`/api/lock/release`, { recordId });
}
getLock(recordId: string) {
return this.http.get<LockInfo>(`/api/lock/${recordId}`);
}
}
8. WebSocket Integration for Real-Time Events
8.1. WebSocket service
@Injectable({ providedIn: 'root' })
export class LockEventsService {
private socket: WebSocket;
private events$ = new Subject<any>();
connect(recordId: string) {
this.socket = new WebSocket(`wss://your-server/ws/locks/${recordId}`);
this.socket.onmessage = msg => {
const data = JSON.parse(msg.data);
this.events$.next(data);
};
this.socket.onclose = () => {
// Reconnect logic (with backoff)
};
}
events(): Observable<any> {
return this.events$.asObservable();
}
}
8.2. Handling reconnection
Use exponential backoff and limit retries to avoid infinite loops.
9. Lock State Management (RxJS Based)
We will keep it simple and avoid full state management libraries.
@Injectable({ providedIn: 'root' })
export class LockState {
private lockInfo$ = new BehaviorSubject<LockInfo | null>(null);
setLockInfo(lock: LockInfo) {
this.lockInfo$.next(lock);
}
getLockInfo() {
return this.lockInfo$.asObservable();
}
}
10. Component-Level Integration
10.1. Resolver to check initial lock state
@Injectable({ providedIn: 'root' })
export class EditCustomerResolver implements Resolve<any> {
constructor(
private lockService: LockService,
private lockState: LockState
) {}
resolve(route: ActivatedRouteSnapshot) {
const id = route.params['id'];
return this.lockService.acquireLock(id).pipe(
tap(lock => this.lockState.setLockInfo(lock)),
catchError(err => {
return this.lockService.getLock(id).pipe(
tap(lock => this.lockState.setLockInfo(lock))
);
})
);
}
}
10.2. Edit component
@Component({
selector: 'app-edit-customer',
templateUrl: './edit-customer.component.html'
})
export class EditCustomerComponent implements OnInit, OnDestroy {
lockInfo: LockInfo;
private destroy$ = new Subject<void>();
constructor(
private lockState: LockState,
private lockEvents: LockEventsService
) {}
ngOnInit() {
this.lockState.getLockInfo()
.pipe(takeUntil(this.destroy$))
.subscribe(info => {
this.lockInfo = info;
});
this.lockEvents.connect(this.lockInfo.recordId);
this.lockEvents.events()
.pipe(takeUntil(this.destroy$))
.subscribe(event => {
if (event.type === 'lock_acquired' || event.type === 'lock_released') {
this.lockState.setLockInfo(event.data);
}
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
11. UI Representation
11.1. Showing lock banner
<div *ngIf="lockInfo && !lockInfo.isOwnedByMe" class="lock-banner">
This record is being edited by {{ lockInfo.lockedByName }}.
</div>
<div *ngIf="lockInfo && lockInfo.isOwnedByMe" class="lock-banner-self">
You are editing this record. Lock will expire at
{{ lockInfo.expiresAt | date:'shortTime' }}.
</div>
11.2. Disabling risky controls
<button [disabled]="!lockInfo?.isOwnedByMe">
Save
</button>
12. Handling Lock Expiration on the Client
Clients must renew locks periodically.
12.1. Auto-renew logic using RxJS interval
@Injectable({ providedIn: 'root' })
export class LockRenewalService {
private renewal$ = new Subject<string>();
constructor(private lockService: LockService) {}
startRenewal(recordId: string) {
interval(30000)
.pipe(
switchMap(() => this.lockService.acquireLock(recordId)),
catchError(() => EMPTY)
)
.subscribe();
}
}
13. Handling Abnormal Situations
Distributed systems fail in unpredictable ways. Production systems must handle failure gracefully.
13.1. Browser tab crash
Servers must detect dropped WebSocket connections and release lock or mark as orphaned.
13.2. Network loss
Angular must detect disconnection and show a local indicator.
13.3. User override
Authorised users should be able to override locks from UI.
13.4. Zombie sessions
If server crashes and loses lock state, client must revalidate before saving.
14. Scalability Considerations
14.1. If you have millions of lock events
Use:
14.2. Keep message size small
Avoid sending complete record content over sockets.
14.3. Horizontal scaling
Lock manager must run in a distributed mode.
Avoid keeping lock state only in memory.
15. Security Considerations
Authenticate every WebSocket connection.
Do not expose internal user IDs publicly.
Use signed tokens for WebSocket authentication.
Validate lock override permissions on the server.
Rate-limit lock requests to avoid flooding.
16. Testing Strategy
16.1. Unit tests
Mock lock service, simulate lock states.
16.2. Integration tests
Simulate two users opening same record.
16.3. End-to-end tests
Use Cypress or Playwright to simulate concurrent browsers.
16.4. Load testing
Use k6 or JMeter to publish thousands of lock events.
17. Observability and Monitoring
A real-time system should include:
Metrics: lock acquisition count, lock conflicts
Logs: overridden locks, expired locks, stale lock cleanup
Dashboards: record-level locking heatmaps
Alerts: lock stuck beyond TTL
18. Real-World Best Practices
18.1. Always use TTL
Never rely purely on client-side cleanup.
18.2. Debounce API calls
Avoid hammering lock API with rapid route changes.
18.3. Use tab identifiers
Helps distinguish same user across multiple tabs.
18.4. Provide clear user messaging
Confusing lock messages cause frustration.
18.5. Ensure graceful fallback
If real-time socket fails, poll API at low frequency.
19. Example Production Scenario
Scenario
An insurance underwriting portal has multiple workflows. Underwriters open claim records, add notes, and update approvals. Often two underwriters open the same claim.
Implementation
Redis stores TTL-based locks.
API writes lock state and publishes events.
WebSocket gateway pushes events to Angular.
Angular shows a lock banner and disables workflow actions.
Locks auto-renew every 60 seconds.
If lock expires after inactivity, another user gains permission to edit.
Outcome
70 percent fewer data-loss incidents
Higher clarity across teams
Faster collaborative resolution of claims
Conclusion
Real-time distributed lock indication in UI is not just a technical feature. It is an essential collaborative capability for any enterprise application handling shared data.
When implemented correctly, it offers:
Angular provides a reliable ecosystem to integrate real-time lock updates with clean UI patterns, and when combined with a strong server-side lock manager, it becomes a production-grade solution fit for complex distributed environments.