Skip to content

Instantly share code, notes, and snippets.

@eladcandroid
Created September 18, 2025 14:46
Show Gist options
  • Save eladcandroid/e7f02371bb95a95cbc783e83210e58a0 to your computer and use it in GitHub Desktop.
Save eladcandroid/e7f02371bb95a95cbc783e83210e58a0 to your computer and use it in GitHub Desktop.
Angular Service Locator Pattern Example - Dynamic service resolution with plugin system and payment providers
// Service Locator Pattern Example - Dynamic Service Resolution
import { Injectable, Injector, Type } from '@angular/core';
/**
* Service Locator Pattern
* Note: This pattern is sometimes considered an anti-pattern because it creates
* hidden dependencies. Use it sparingly and prefer Angular's DI when possible.
* Good use cases: Plugin systems, dynamic service loading, runtime service selection
*/
// SERVICE LOCATOR - Central registry for services
@Injectable({
providedIn: 'root'
})
export class ServiceLocator {
private static instance: ServiceLocator;
private services = new Map<string, any>();
constructor(private injector: Injector) {
ServiceLocator.instance = this;
console.log('ServiceLocator initialized');
}
// Get the singleton instance
static getInstance(): ServiceLocator {
if (!ServiceLocator.instance) {
throw new Error('ServiceLocator not initialized. Ensure it is provided in root module.');
}
return ServiceLocator.instance;
}
// Register a service instance with a token
register<T>(token: string, service: T): void {
if (this.services.has(token)) {
console.warn(`Service ${token} is already registered. Overwriting...`);
}
this.services.set(token, service);
console.log(`Service registered: ${token}`);
}
// Get a service by token
get<T>(token: string): T {
// Check registered services first
if (this.services.has(token)) {
return this.services.get(token) as T;
}
// Try to get from Angular's injector
try {
const service = this.injector.get(token as any);
this.services.set(token, service); // Cache it
return service;
} catch (error) {
throw new Error(`Service ${token} not found in ServiceLocator or Angular DI`);
}
}
// Get service by type (class)
getByType<T>(type: Type<T>): T {
const token = type.name;
// Check if already registered by type name
if (this.services.has(token)) {
return this.services.get(token);
}
// Try to get from Angular's injector
try {
const service = this.injector.get(type);
this.services.set(token, service); // Cache with type name
return service;
} catch (error) {
throw new Error(`Service ${type.name} not found`);
}
}
// Check if service exists
has(token: string): boolean {
return this.services.has(token);
}
// Unregister a service
unregister(token: string): void {
if (this.services.has(token)) {
this.services.delete(token);
console.log(`Service unregistered: ${token}`);
}
}
// Clear all registered services
clear(): void {
this.services.clear();
console.log('All services cleared from ServiceLocator');
}
// List all registered services
listServices(): string[] {
return Array.from(this.services.keys());
}
}
// PLUGIN SYSTEM USING SERVICE LOCATOR
// Plugin Interface
export interface Plugin {
name: string;
version: string;
description: string;
initialize(): Promise<void>;
execute(data: any): any;
destroy?(): void;
}
// Plugin Manager using Service Locator
@Injectable({
providedIn: 'root'
})
export class PluginManager {
private plugins = new Map<string, Plugin>();
private pluginStates = new Map<string, 'loading' | 'active' | 'error'>();
constructor(private serviceLocator: ServiceLocator) {}
// Register and initialize a plugin
async registerPlugin(plugin: Plugin): Promise<void> {
if (this.plugins.has(plugin.name)) {
throw new Error(`Plugin ${plugin.name} is already registered`);
}
console.log(`Registering plugin: ${plugin.name} v${plugin.version}`);
this.pluginStates.set(plugin.name, 'loading');
try {
await plugin.initialize();
this.plugins.set(plugin.name, plugin);
this.serviceLocator.register(`plugin:${plugin.name}`, plugin);
this.pluginStates.set(plugin.name, 'active');
console.log(`Plugin activated: ${plugin.name}`);
} catch (error) {
this.pluginStates.set(plugin.name, 'error');
throw new Error(`Failed to initialize plugin ${plugin.name}: ${error}`);
}
}
// Get a plugin by name
getPlugin(name: string): Plugin | undefined {
return this.serviceLocator.get<Plugin>(`plugin:${name}`);
}
// Execute a plugin
async executePlugin(name: string, data: any): Promise<any> {
const plugin = this.getPlugin(name);
if (!plugin) {
throw new Error(`Plugin ${name} not found`);
}
if (this.pluginStates.get(name) !== 'active') {
throw new Error(`Plugin ${name} is not active`);
}
return plugin.execute(data);
}
// Unregister a plugin
async unregisterPlugin(name: string): Promise<void> {
const plugin = this.plugins.get(name);
if (plugin) {
if (plugin.destroy) {
await plugin.destroy();
}
this.plugins.delete(name);
this.serviceLocator.unregister(`plugin:${name}`);
this.pluginStates.delete(name);
console.log(`Plugin unregistered: ${name}`);
}
}
// List all registered plugins
listPlugins(): Array<{name: string, version: string, state: string}> {
return Array.from(this.plugins.entries()).map(([name, plugin]) => ({
name: plugin.name,
version: plugin.version,
state: this.pluginStates.get(name) || 'unknown'
}));
}
// Check if plugin exists and is active
isPluginActive(name: string): boolean {
return this.pluginStates.get(name) === 'active';
}
}
// EXAMPLE PLUGINS
// Analytics Plugin
export class AnalyticsPlugin implements Plugin {
name = 'analytics';
version = '1.0.0';
description = 'Tracks user interactions and events';
private events: any[] = [];
async initialize(): Promise<void> {
console.log('Analytics plugin initializing...');
// Load analytics library, configure, etc.
await this.loadAnalyticsLibrary();
}
execute(data: { action: string; category: string; label?: string; value?: number }): void {
const event = {
timestamp: new Date(),
...data
};
this.events.push(event);
console.log('Analytics event tracked:', event);
// Send to analytics service
this.sendToAnalytics(event);
}
private async loadAnalyticsLibrary(): Promise<void> {
// Simulate loading external library
return new Promise(resolve => setTimeout(resolve, 100));
}
private sendToAnalytics(event: any): void {
// Send to Google Analytics, Mixpanel, etc.
if (window['gtag']) {
window['gtag']('event', event.action, {
event_category: event.category,
event_label: event.label,
value: event.value
});
}
}
getEvents(): any[] {
return [...this.events];
}
destroy(): void {
this.events = [];
console.log('Analytics plugin destroyed');
}
}
// Chat Plugin
export class ChatPlugin implements Plugin {
name = 'chat';
version = '2.0.0';
description = 'Provides live chat functionality';
private chatWidget: any;
async initialize(): Promise<void> {
console.log('Chat plugin initializing...');
await this.loadChatWidget();
}
execute(data: { action: 'show' | 'hide' | 'message'; payload?: any }): any {
switch (data.action) {
case 'show':
return this.showChat();
case 'hide':
return this.hideChat();
case 'message':
return this.sendMessage(data.payload);
default:
throw new Error(`Unknown chat action: ${data.action}`);
}
}
private async loadChatWidget(): Promise<void> {
// Simulate loading chat widget
return new Promise(resolve => {
setTimeout(() => {
this.chatWidget = { isLoaded: true };
resolve();
}, 200);
});
}
private showChat(): void {
console.log('Showing chat widget');
// Implementation
}
private hideChat(): void {
console.log('Hiding chat widget');
// Implementation
}
private sendMessage(message: string): void {
console.log('Sending message:', message);
// Implementation
}
destroy(): void {
this.chatWidget = null;
console.log('Chat plugin destroyed');
}
}
// PAYMENT SYSTEM WITH SERVICE LOCATOR
// Payment Provider Interface
export interface PaymentProvider {
name: string;
supportedCurrencies: string[];
processPayment(amount: number, currency: string, details: any): Promise<PaymentResult>;
validatePaymentDetails(details: any): boolean;
getTransactionFee(amount: number): number;
}
export interface PaymentResult {
success: boolean;
transactionId?: string;
error?: string;
timestamp: Date;
}
// Payment Manager using Service Locator for Dynamic Provider Selection
@Injectable({
providedIn: 'root'
})
export class PaymentManager {
private providers = new Map<string, PaymentProvider>();
constructor(private serviceLocator: ServiceLocator) {
this.initializeProviders();
}
private initializeProviders(): void {
// Register default payment providers
this.registerProvider(new PayPalProvider());
this.registerProvider(new StripeProvider());
this.registerProvider(new CreditCardProvider());
}
registerProvider(provider: PaymentProvider): void {
this.providers.set(provider.name, provider);
this.serviceLocator.register(`payment:${provider.name}`, provider);
console.log(`Payment provider registered: ${provider.name}`);
}
getProvider(name: string): PaymentProvider {
const provider = this.serviceLocator.get<PaymentProvider>(`payment:${name}`);
if (!provider) {
throw new Error(`Payment provider ${name} not found`);
}
return provider;
}
async processPayment(
providerName: string,
amount: number,
currency: string,
details: any
): Promise<PaymentResult> {
const provider = this.getProvider(providerName);
// Validate payment details
if (!provider.validatePaymentDetails(details)) {
return {
success: false,
error: 'Invalid payment details',
timestamp: new Date()
};
}
// Check currency support
if (!provider.supportedCurrencies.includes(currency)) {
return {
success: false,
error: `Currency ${currency} not supported by ${providerName}`,
timestamp: new Date()
};
}
// Process payment
try {
const result = await provider.processPayment(amount, currency, details);
console.log(`Payment processed via ${providerName}:`, result);
return result;
} catch (error) {
return {
success: false,
error: error.message,
timestamp: new Date()
};
}
}
getAvailableProviders(): string[] {
return Array.from(this.providers.keys());
}
getProviderFee(providerName: string, amount: number): number {
const provider = this.getProvider(providerName);
return provider.getTransactionFee(amount);
}
}
// PayPal Provider Implementation
class PayPalProvider implements PaymentProvider {
name = 'paypal';
supportedCurrencies = ['USD', 'EUR', 'GBP'];
async processPayment(amount: number, currency: string, details: any): Promise<PaymentResult> {
console.log(`Processing PayPal payment: ${amount} ${currency}`);
// Simulate API call
await this.simulateApiCall();
return {
success: true,
transactionId: `PP-${Date.now()}`,
timestamp: new Date()
};
}
validatePaymentDetails(details: any): boolean {
return details && details.email && details.password;
}
getTransactionFee(amount: number): number {
return amount * 0.029 + 0.30; // 2.9% + $0.30
}
private simulateApiCall(): Promise<void> {
return new Promise(resolve => setTimeout(resolve, 1000));
}
}
// Stripe Provider Implementation
class StripeProvider implements PaymentProvider {
name = 'stripe';
supportedCurrencies = ['USD', 'EUR', 'GBP', 'JPY', 'CAD'];
async processPayment(amount: number, currency: string, details: any): Promise<PaymentResult> {
console.log(`Processing Stripe payment: ${amount} ${currency}`);
await this.simulateApiCall();
return {
success: true,
transactionId: `STR-${Date.now()}`,
timestamp: new Date()
};
}
validatePaymentDetails(details: any): boolean {
return details && details.cardNumber && details.cvv && details.expiryDate;
}
getTransactionFee(amount: number): number {
return amount * 0.027 + 0.25; // 2.7% + $0.25
}
private simulateApiCall(): Promise<void> {
return new Promise(resolve => setTimeout(resolve, 800));
}
}
// Credit Card Provider Implementation
class CreditCardProvider implements PaymentProvider {
name = 'creditcard';
supportedCurrencies = ['USD', 'EUR'];
async processPayment(amount: number, currency: string, details: any): Promise<PaymentResult> {
console.log(`Processing Credit Card payment: ${amount} ${currency}`);
await this.simulateApiCall();
return {
success: true,
transactionId: `CC-${Date.now()}`,
timestamp: new Date()
};
}
validatePaymentDetails(details: any): boolean {
return details &&
details.cardNumber &&
details.cardholderName &&
details.cvv &&
details.expiryMonth &&
details.expiryYear;
}
getTransactionFee(amount: number): number {
return amount * 0.025 + 0.10; // 2.5% + $0.10
}
private simulateApiCall(): Promise<void> {
return new Promise(resolve => setTimeout(resolve, 1500));
}
}
// USAGE EXAMPLE - Component using Service Locator
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-service-locator-demo',
template: `
<div class="demo-container">
<h2>Service Locator Pattern Demo</h2>
<!-- Plugin Management -->
<div class="section">
<h3>Plugin Management</h3>
<div class="plugins">
<button (click)="loadAnalyticsPlugin()">Load Analytics Plugin</button>
<button (click)="loadChatPlugin()">Load Chat Plugin</button>
<button (click)="listPlugins()">List Plugins</button>
</div>
<div class="plugin-actions" *ngIf="activePlugins.length > 0">
<h4>Active Plugins:</h4>
<div *ngFor="let plugin of activePlugins" class="plugin-item">
{{ plugin.name }} v{{ plugin.version }} ({{ plugin.state }})
<button (click)="executeAnalytics()" *ngIf="plugin.name === 'analytics'">
Track Event
</button>
<button (click)="executeChat()" *ngIf="plugin.name === 'chat'">
Show Chat
</button>
</div>
</div>
</div>
<!-- Payment Processing -->
<div class="section">
<h3>Payment Processing</h3>
<div class="payment-options">
<select [(ngModel)]="selectedProvider">
<option value="">Select Payment Method</option>
<option *ngFor="let provider of availableProviders" [value]="provider">
{{ provider }}
</option>
</select>
<input type="number" [(ngModel)]="paymentAmount" placeholder="Amount" />
<select [(ngModel)]="paymentCurrency">
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="GBP">GBP</option>
</select>
<button (click)="processPayment()" [disabled]="!selectedProvider || !paymentAmount">
Process Payment
</button>
</div>
<div class="payment-result" *ngIf="paymentResult">
<h4>Payment Result:</h4>
<p>Status: {{ paymentResult.success ? 'Success' : 'Failed' }}</p>
<p *ngIf="paymentResult.transactionId">
Transaction ID: {{ paymentResult.transactionId }}
</p>
<p *ngIf="paymentResult.error">Error: {{ paymentResult.error }}</p>
<p>Fee: ${{ transactionFee.toFixed(2) }}</p>
</div>
</div>
<!-- Service Locator Status -->
<div class="section">
<h3>Service Locator Status</h3>
<p>Registered Services: {{ registeredServices.length }}</p>
<ul>
<li *ngFor="let service of registeredServices">{{ service }}</li>
</ul>
</div>
</div>
`,
styles: [`
.demo-container {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
h3 {
margin-top: 0;
color: #333;
}
button {
margin: 5px;
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
select, input {
margin: 5px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.plugin-item {
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-radius: 4px;
}
.payment-result {
margin-top: 20px;
padding: 15px;
background: #f0f8ff;
border-radius: 4px;
}
`]
})
export class ServiceLocatorDemoComponent implements OnInit {
activePlugins: any[] = [];
availableProviders: string[] = [];
selectedProvider: string = '';
paymentAmount: number = 100;
paymentCurrency: string = 'USD';
paymentResult: PaymentResult | null = null;
transactionFee: number = 0;
registeredServices: string[] = [];
constructor(
private serviceLocator: ServiceLocator,
private pluginManager: PluginManager,
private paymentManager: PaymentManager
) {}
ngOnInit(): void {
this.loadAvailableProviders();
this.updateRegisteredServices();
}
// Plugin Management
async loadAnalyticsPlugin(): Promise<void> {
try {
const plugin = new AnalyticsPlugin();
await this.pluginManager.registerPlugin(plugin);
this.updatePluginList();
this.updateRegisteredServices();
console.log('Analytics plugin loaded successfully');
} catch (error) {
console.error('Failed to load analytics plugin:', error);
}
}
async loadChatPlugin(): Promise<void> {
try {
const plugin = new ChatPlugin();
await this.pluginManager.registerPlugin(plugin);
this.updatePluginList();
this.updateRegisteredServices();
console.log('Chat plugin loaded successfully');
} catch (error) {
console.error('Failed to load chat plugin:', error);
}
}
listPlugins(): void {
this.updatePluginList();
console.log('Active plugins:', this.activePlugins);
}
async executeAnalytics(): Promise<void> {
try {
await this.pluginManager.executePlugin('analytics', {
action: 'button_click',
category: 'demo',
label: 'service_locator_test',
value: 1
});
console.log('Analytics event tracked');
} catch (error) {
console.error('Failed to execute analytics plugin:', error);
}
}
async executeChat(): Promise<void> {
try {
await this.pluginManager.executePlugin('chat', {
action: 'show'
});
console.log('Chat widget shown');
} catch (error) {
console.error('Failed to execute chat plugin:', error);
}
}
// Payment Processing
loadAvailableProviders(): void {
this.availableProviders = this.paymentManager.getAvailableProviders();
}
async processPayment(): Promise<void> {
if (!this.selectedProvider || !this.paymentAmount) {
return;
}
// Calculate fee
this.transactionFee = this.paymentManager.getProviderFee(
this.selectedProvider,
this.paymentAmount
);
// Prepare payment details based on provider
const paymentDetails = this.getPaymentDetails(this.selectedProvider);
// Process payment
this.paymentResult = await this.paymentManager.processPayment(
this.selectedProvider,
this.paymentAmount,
this.paymentCurrency,
paymentDetails
);
}
private getPaymentDetails(provider: string): any {
switch (provider) {
case 'paypal':
return { email: '[email protected]', password: 'secure123' };
case 'stripe':
return { cardNumber: '4242424242424242', cvv: '123', expiryDate: '12/25' };
case 'creditcard':
return {
cardNumber: '1234567890123456',
cardholderName: 'John Doe',
cvv: '456',
expiryMonth: '12',
expiryYear: '2025'
};
default:
return {};
}
}
// Service Locator Status
private updatePluginList(): void {
this.activePlugins = this.pluginManager.listPlugins();
}
private updateRegisteredServices(): void {
this.registeredServices = this.serviceLocator.listServices();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment