Angular  

Real-Time Distributed Lock Indication in UI using Angular

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:

  • Data corruption

  • Lost edits

  • Confusing user experience

  • Customer service errors

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

  1. Lock Manager (API layer or distributed cache)

  2. Pub/Sub or event distribution system

  3. WebSocket or SSE layer for real-time events

  4. Angular components and services

  5. 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

  1. If lock not present, grant.

  2. If lock exists and user is same (same tab), renew.

  3. If lock exists but expired, override.

  4. If lock exists and active, return lock info and deny.

5.4. Lock release

Triggered by:

  • Explicit release API call

  • Session disconnect event

  • TTL expiration

  • Administrative override

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:

  1. Lock service

  2. WebSocket service

  3. A lock state store

  4. Angular resolvers and guards

  5. UI component integration

  6. 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:

  • Redis Streams

  • Kafka partitions

  • WebSocket sharding

  • Distributed gateway services

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

  1. Authenticate every WebSocket connection.

  2. Do not expose internal user IDs publicly.

  3. Use signed tokens for WebSocket authentication.

  4. Validate lock override permissions on the server.

  5. 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

  1. Redis stores TTL-based locks.

  2. API writes lock state and publishes events.

  3. WebSocket gateway pushes events to Angular.

  4. Angular shows a lock banner and disables workflow actions.

  5. Locks auto-renew every 60 seconds.

  6. 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:

  • Predictable behaviour

  • Clear user experience

  • Protection from conflicts

  • Smooth multi-user collaboration

  • Operational visibility

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.