Skip to content

Instantly share code, notes, and snippets.

@DattatreyaReddy
Last active March 18, 2026 04:54
Show Gist options
  • Select an option

  • Save DattatreyaReddy/999dde7808f27889f3e4641e0939a5ab to your computer and use it in GitHub Desktop.

Select an option

Save DattatreyaReddy/999dde7808f27889f3e4641e0939a5ab to your computer and use it in GitHub Desktop.
Client Service
/// Old way
@Component
public class PaymentServiceFactory {
@Autowired private FkPaymentService fkService;
@Autowired private CtPaymentService ctService;
public PaymentService getService(String client) {
switch (client) {
case "FK": return fkService;
case "CT": return ctService;
default: throw new IllegalArgumentException();
}
}
}
/// New Way
// Simple case: single client, service type inferred from interface
@Client("clientA")
@Service
public class ClientAPaymentService implements PaymentService { ... }
// Explicit service type specification
@Client(value = { "clientB" }, type = PaymentService.class)
@Service
public class ClientBPaymentService implements PaymentService { ... }
// Multiple clients sharing the same implementation
@Client( { "clientC", "clientD" } )
@Service
public class SharedPaymentService implements PaymentService { ... }
// Usage:
@Autowired
private ClientRegistry clientRegistry
PaymentService service = clientRegistry.getBean("FK", PaymentService.class);
service.process()
import java.lang.annotation.*;
/**
* Annotation used to mark beans that are client-specific implementations.
* Beans annotated with this annotation are automatically discovered and
* registered
* by the {@link ClientRegistry} for client-based bean lookup.
* <p>
* This annotation enables a multi-tenant bean architecture where multiple
* implementations of the same bean interface can coexist, each mapped to one
* or more client identifiers. The registry uses the client identifier to
* retrieve
* the appropriate bean implementation at runtime.
* <p>
* <strong>Type Resolution:</strong>
* <ul>
* <li>The type can be explicitly specified via the {@link #type()}
* attribute</li>
* <li>If not specified, it will be automatically inferred from the bean's
* interfaces or superclass</li>
* <li>If the bean implements multiple interfaces, the type must be
* explicitly specified to avoid ambiguity</li>
* <li>If the bean doesn't implement any interface or extend any class (other
* than Object), the bean's own class will be used as the type</li>
* </ul>
* <p>
* <strong>Client Mapping:</strong>
* A single bean can be mapped to multiple clients by providing an array of
* client
* identifiers in the {@link #value()} attribute. This is useful when multiple
* clients share the same type implementation.
* <p>
* <strong>Example usage:</strong>
*
* <pre>
* {@code
* // Simple case: single client, type inferred from interface
* @Client("clientA")
* @Service
* public class ClientAPaymentService implements PaymentService {...}
*
* // Explicit type specification
* @Client(value = "clientB", type = PaymentService.class)
* @Service
* public class ClientBPaymentService implements PaymentService {...}
*
* // Multiple clients sharing the same implementation
* @Client( { "clientC", "clientD" } )
* @Service
* public class SharedPaymentService implements PaymentService {...}
* }
* </pre>
*
* <p>
* <strong>Requirements:</strong>
* <ul>
* <li>Beans must be Spring-managed (annotated with {@code @Service},
* {@code @Component}, etc.)</li>
* <li>Beans must be singleton scope (prototype scope is not supported)</li>
* <li>Client identifiers must be non-empty strings</li>
* <li>Each client-type combination must be unique (no duplicates
* allowed)</li>
* </ul>
*
* @see ClientRegistry
* @author Panta Dattatreya Reddy
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Client {
/**
* The client identifier(s) for this bean.
* This value is used to map the bean to one or more specific clients.
* <p>
* A single bean can be mapped to multiple clients by providing multiple
* identifiers. This is useful when multiple clients share the same bean
* implementation.
* <p>
* <strong>Example:</strong>
*
* <pre>
* {@code
*
* @Client( { "clientA", "clientB" } ) // Maps to both clientA and clientB
* @Service
* public class SharedService implements PaymentService {...}
* }
* </pre>
*
* @return array of client identifiers (must be non-empty)
*/
String[] value();
/**
* The class type that this bean implements or extends.
* <p>
* If not specified (defaults to {@code Void.class}), the registry will attempt
* to automatically determine the type using the following rules:
* <ol>
* <li>If the bean implements exactly one interface, that interface is used</li>
* <li>If the bean implements multiple interfaces, an exception is thrown
* (explicit specification required)</li>
* <li>If the bean implements no interfaces, the superclass (if not Object)
* is used</li>
* <li>If none of the above apply, the bean's own class is used as the type</li>
* </ol>
* <p>
* <strong>When to specify explicitly:</strong>
* <ul>
* <li>Bean implements multiple interfaces</li>
* <li>You want to be explicit about the contract</li>
* </ul>
*
* @return the type of bean, or {@code Void.class} if not specified
*/
Class<?> type() default Void.class;
}
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* Registry for retrieving client-specific bean implementations.
* <p>
* This registry provides a mechanism to retrieve beans based on both
* the type and a client identifier. It automatically discovers and
* registers all beans annotated with {@link Client} during Spring
* initialization.
* <p>
* <strong>Architecture:</strong>
* The registry maintains an internal immutable map structure:
* <p>
* {@code Map<Class<Type>, Map<ClientId, BeanInstance>>}
* <p>
* This allows efficient O(1) lookup of beans by client and type.
* <p>
* <strong>Usage Example:</strong>
*
* <pre>{@code
* @Autowired
* private ClientRegistry clientRegistry;
*
* public void processPayment(String clientId) {
* PaymentService paymentService = clientRegistry.getBean(clientId, PaymentService.class);
* paymentService.process();
* }
* }</pre>
* <p>
* <strong>Thread Safety:</strong>
* This registry is thread-safe. The internal maps are immutable after
* initialization, and all bean lookups are read-only operations.
* <p>
* <strong>Initialization:</strong>
* The registry initializes during Spring's bean lifecycle:
* <ol>
* <li>{@link #setApplicationContext(ApplicationContext)} is called to
* provide access to the Spring context</li>
* <li>{@link #afterPropertiesSet()} is called after dependency injection,
* which triggers the discovery and registration of all
* {@code @Client} annotated beans</li>
* </ol>
* <p>
* <strong>Error Handling:</strong>
* The registry performs comprehensive validation during initialization:
* <ul>
* <li>Validates that beans are singleton scope (prototypes not supported)</li>
* <li>Validates client identifiers are non-empty</li>
* <li>Detects and prevents duplicate client-type mappings</li>
* <li>Validates type compatibility between beans and declared types</li>
* <li>Handles ambiguous types (multiple interfaces) with clear error messages</li>
* </ul>
*
* @see Client
* @author Panta Dattatreya Reddy
*/
@Component
public class ClientRegistry implements ApplicationContextAware, InitializingBean {
/**
* Internal immutable map structure for efficient bean lookup.
* <p>
* Structure: {@code Map<Class<Type>, Map<ClientId, BeanInstance>>}
* <p>
* This map is populated during initialization and remains immutable
* thereafter for thread safety. All beans must be singleton scope.
*/
private Map<Class<?>, Map<String, Object>> typeBeanMap = new HashMap<>();
private ListableBeanFactory beanFactory;
/**
* Retrieves a bean for the specified client and type.
* <p>
* This method performs a lookup in the internal map structure to find the
* appropriate type implementation for the given client. The lookup is
* thread-safe and efficient (O(1) complexity).
* <p>
* <strong>Example:</strong>
*
* <pre>{@code
* PaymentService service = registry.getBean("clientA", PaymentService.class);
* }</pre>
*
* @param client the client identifier (must be non-null and non-empty)
* @param type the type class (must be non-null)
* @param <Type> the type
* @return the bean instance (never null)
* @throws IllegalArgumentException if the client identifier is null or
* empty, or if type is null
* @throws NoSuchBeanDefinitionException if no bean is found for the specified
* client and type combination
*/
public <Type> @NotNull Type getBean(String client, Class<Type> type) {
if (StringUtils.isBlank(client)) {
throw new IllegalArgumentException("Client identifier cannot be null or empty");
}
if (type == null) {
throw new IllegalArgumentException("Type cannot be null. Please provide a valid bean type.");
}
var bean = Optional.ofNullable(typeBeanMap.get(type))
.map(clientMap -> clientMap.get(client))
.orElseThrow(() -> new NoSuchBeanDefinitionException(
String.format("No bean found for client '%s' and type: %s.class",
client, type.getSimpleName())));
// The cast is now safe because we validated type compatibility during init
return type.cast(bean);
}
/**
* Initializes the type bean map after all properties have been set.
* <p>
* This method is called by Spring after dependency injection is complete
* (as part of the {@link InitializingBean} lifecycle). It performs the
* following operations:
* <ol>
* <li>Scans the application context for all beans annotated with
* {@link Client}</li>
* <li>Validates bean configurations (scope, client identifiers, type
* compatibility)</li>
* <li>Builds an immutable map structure for efficient bean retrieval</li>
* </ol>
* <p>
* After this method completes, the registry is ready to serve bean lookup
* requests.
*
* @throws IllegalStateException if duplicate client-type mappings are
* found, ambiguous types are detected,
* or beans have invalid configurations
* @throws IllegalArgumentException if prototype beans are found or validation
* fails
*/
@Override
public void afterPropertiesSet() {
initializeTypeBeanMap();
}
/**
* Sets the Spring ApplicationContext for this registry.
* <p>
* This method is called by Spring during bean initialization (as part of the
* {@link ApplicationContextAware} interface) to provide access to the
* application context. The context is used to discover and access beans
* annotated with {@link Client}.
* <p>
* The context is stored as a {@link ListableBeanFactory} to limit the scope
* of operations to only what is needed for bean discovery.
*
* @param applicationContext the Spring ApplicationContext instance (never null)
* @throws BeansException if an error occurs while setting the context
*/
@Override
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
// ApplicationContext extends ListableBeanFactory, so this assignment is valid
// using ListableBeanFactory to avoid unnecessary scope expansion
this.beanFactory = applicationContext;
}
/**
* Initializes the internal type bean map by scanning all beans annotated
* with {@link Client}.
* <p>
* This method performs the following operations:
* <ol>
* <li><strong>Discovery:</strong> Finds all beans with the
* {@code @Client} annotation using Spring's bean registry</li>
* <li><strong>Scope Validation:</strong> Ensures all beans are singleton
* scope (prototype scope is not supported)</li>
* <li><strong>Type Resolution:</strong> Determines the type
* from the annotation's {@code type} attribute, or infers it from
* the bean's interfaces or superclass</li>
* <li><strong>Validation:</strong> Validates client identifiers are
* non-empty and checks type compatibility</li>
* <li><strong>Duplicate Detection:</strong> Ensures no duplicate
* client-type mappings exist</li>
* <li><strong>Map Construction:</strong> Builds an immutable map structure
* for efficient O(1) bean lookup</li>
* </ol>
* <p>
* The resulting map structure is:
* {@code Map<Type, Map<ClientId, BeanInstance>>}
* <p>
* * All maps are made unmodifiable for thread safety.
*
* @throws IllegalArgumentException if prototype beans are found, client
* identifiers are empty, or validation fails
* @throws IllegalStateException if duplicate client-type mappings exist,
* ambiguous types are detected
* (multiple interfaces), or beans don't
* implement/extend the specified type
*/
private void initializeTypeBeanMap() {
var beansWithAnnotation = beanFactory.getBeansWithAnnotation(Client.class);
var tempMap = new HashMap<Class<?>, Map<String, Object>>();
beansWithAnnotation.forEach((beanName, beanInstance) -> {
if (beanFactory.isPrototype(beanName)) {
throw new IllegalArgumentException(String.format(
"Bean %s.class is configured as a prototype scope, which is not supported. " +
"@Client annotated beans must be singleton scope.",
beanInstance.getClass().getSimpleName()));
}
var annotation = beanFactory.findAnnotationOnBean(beanName, Client.class);
// Defensive check (though getBeansWithAnnotation usually guarantees presence)
if (annotation == null)
return;
var clients = annotation.value();
validateAnnotation(beanInstance, clients);
var type = getType(beanInstance, annotation);
validateTypeCompatibility(beanInstance, type);
// Compute inner map
var clientMap = tempMap.computeIfAbsent(type, k -> new HashMap<>());
for (var client : clients)
addBeanToClientMap(beanInstance, client, clientMap, type);
});
// Make maps unmodifiable for thread safety/immutability
tempMap.replaceAll((k, v) -> Collections.unmodifiableMap(v));
this.typeBeanMap = Collections.unmodifiableMap(tempMap);
}
/**
* Adds a bean instance to the client map, performing duplicate detection.
* <p>
* This method ensures that each client-type combination is unique. If a
* duplicate mapping is detected, an exception is thrown with detailed
* information about the conflicting beans.
*
* @param beanInstance the bean instance to add
* @param client the client identifier
* @param clientMap the map of client IDs to bean instances for this type
* @param type the type class
* @throws IllegalStateException if a duplicate client-type mapping is
* detected
*/
private void addBeanToClientMap(Object beanInstance, String client, Map<String, Object> clientMap, Class<?> type) {
// DUPLICATE CHECK: Fail fast if two beans define the same client+type
if (clientMap.containsKey(client)) {
var existingBean = clientMap.get(client);
throw new IllegalStateException(
String.format("Duplicate @Client definition detected. " +
"Client '%s' for type %s.class is already mapped to bean class %s.class. " +
"Conflicting bean: %s.class. Each client-type combination must be unique.",
client, type.getSimpleName(),
existingBean.getClass().getSimpleName(),
beanInstance.getClass().getSimpleName()));
}
clientMap.put(client, beanInstance);
}
/**
* Determines the type for a bean instance.
* <p>
* This method resolves the type using the following priority:
* <ol>
* <li>If explicitly specified in the annotation's {@code type} attribute
* (and not {@code Void.class}), use that value</li>
* <li>If the bean implements exactly one interface, use that interface</li>
* <li>If the bean implements multiple interfaces, throw an exception
* (ambiguity)</li>
* <li>If the bean has a superclass (other than Object), use that
* superclass</li>
* <li>Otherwise, use the bean's own class as the type</li>
* </ol>
* <p>
* This method handles AOP proxies correctly by using
* {@link AopUtils#getTargetClass(Object)} to unwrap proxy objects.
*
* @param beanInstance the bean instance to analyze
* @param annotation the {@link Client} annotation from the bean
* @return the resolved type class (never null)
* @throws IllegalStateException if the type cannot be determined
* (ambiguous - multiple interfaces)
*/
private static Class<?> getType(Object beanInstance, Client annotation) {
var type = annotation.type();
// If type is null, extract immediate interface or class of the annotated
// class
if (type == null || type == Void.class) {
var targetClass = AopUtils.getTargetClass(beanInstance);
var interfaces = targetClass.getInterfaces();
if (interfaces.length == 1) {
type = interfaces[0];
} else if (interfaces.length > 1) {
// AMBIGUITY CHECK
throw new IllegalStateException(String.format(
"Bean %s.class implements multiple interfaces %s, making the type ambiguous. " +
"Please explicitly specify the target type using the 'type' attribute " +
"in the @Client annotation " +
"(e.g., @Client(value = \"%s\", type = %s.class)).",
beanInstance.getClass().getSimpleName(), Arrays.toString(interfaces),
Arrays.toString(annotation.value()), interfaces[0].getSimpleName()));
} else {
// Otherwise use the parent class (superclass)
var superClass = targetClass.getSuperclass();
if (superClass != null && superClass != Object.class) {
type = superClass;
} else {
// If no interface or superclass found, use the bean's own class as the type
type = targetClass;
}
}
}
return type;
}
/**
* Validates that the client identifier(s) in the {@link Client}
* annotation are not null or empty.
* <p>
* This method ensures that:
* <ul>
* <li>The clients array is not empty</li>
* <li>Each client identifier in the array is not null or blank</li>
* </ul>
*
* @param beanInstance the bean instance being validated (used for error
* messages)
* @param clients the client identifiers from the annotation
* @throws IllegalStateException if the clients array is empty or contains any
* null or blank identifiers
*/
private void validateAnnotation(Object beanInstance, String[] clients) {
// Cache class name to avoid repeated method calls
var errorMessage = String.format(
"Bean %s.class has @Client annotation with empty client identifier. " +
"The 'value' attribute must specify a non-empty client identifier.",
beanInstance.getClass().getSimpleName());
if (clients.length == 0) {
throw new IllegalStateException(errorMessage);
}
for (var client : clients) {
if (StringUtils.isBlank(client)) {
throw new IllegalStateException(errorMessage);
}
}
}
/**
* Validates that the bean instance actually implements or extends the
* type declared in the annotation.
* <p>
* This method uses {@link AopUtils#getTargetClass(Object)} to handle AOP
* proxies correctly. It unwraps CGLIB/JDK proxies to check the actual
* underlying class and its interfaces.
* <p>
* The validation ensures type safety by verifying that the bean can be safely
* cast to the declared type.
*
* @param beanInstance the bean instance to validate
* @param type the type class that the bean should implement or extend
* @throws IllegalStateException if the bean does not implement or extend the
* specified type
*/
private void validateTypeCompatibility(Object beanInstance, Class<?> type) {
// AopUtils.getTargetClass unwraps CGLIB/JDK proxies to check the actual
// underlying class/interfaces
var targetClass = AopUtils.getTargetClass(beanInstance);
if (!type.isAssignableFrom(targetClass)) {
throw new IllegalStateException(
String.format("Bean %s.class is annotated with type %s.class "
+ "but does not implement or extend it. "
+ "Please ensure the bean class implements the specified "
+ "interface or extends the specified class type.",
beanInstance.getClass().getSimpleName(), type.getSimpleName()));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment