As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
Java's Service Provider Interface represents one of the most powerful yet underutilized patterns for creating flexible software architectures. I've spent years working with various extensibility patterns, and SPI consistently proves its worth in enterprise applications where modularity and adaptability matter most.
The fundamental concept behind SPI revolves around separating service definitions from their implementations. This separation creates opportunities for dynamic behavior modification, plugin architectures, and seamless integration of third-party components. Modern applications demand this flexibility to remain competitive and maintainable.
Understanding the Foundation
SPI operates through a simple yet elegant mechanism. You define service contracts using interfaces, implement these contracts in separate classes, and register implementations through META-INF configuration files. The Java runtime handles discovery and instantiation automatically.
public interface PaymentProcessor {
boolean processPayment(PaymentRequest request);
String getProviderName();
boolean supports(PaymentMethod method);
}
public class CreditCardProcessor implements PaymentProcessor {
@Override
public boolean processPayment(PaymentRequest request) {
// Credit card processing logic
System.out.println("Processing credit card payment: " + request.getAmount());
return true;
}
@Override
public String getProviderName() {
return "CreditCard";
}
@Override
public boolean supports(PaymentMethod method) {
return method == PaymentMethod.CREDIT_CARD;
}
}
public class PayPalProcessor implements PaymentProcessor {
@Override
public boolean processPayment(PaymentRequest request) {
// PayPal processing logic
System.out.println("Processing PayPal payment: " + request.getAmount());
return true;
}
@Override
public String getProviderName() {
return "PayPal";
}
@Override
public boolean supports(PaymentMethod method) {
return method == PaymentMethod.PAYPAL;
}
}
The configuration file /META-INF/services/com.example.PaymentProcessor
lists available implementations:
com.example.CreditCardProcessor
com.example.PayPalProcessor
Loading and using these services becomes straightforward:
public class PaymentService {
private final Map<PaymentMethod, PaymentProcessor> processors;
public PaymentService() {
processors = new HashMap<>();
loadProcessors();
}
private void loadProcessors() {
ServiceLoader<PaymentProcessor> loader = ServiceLoader.load(PaymentProcessor.class);
for (PaymentProcessor processor : loader) {
System.out.println("Loaded processor: " + processor.getProviderName());
// Register processor for supported methods
for (PaymentMethod method : PaymentMethod.values()) {
if (processor.supports(method)) {
processors.put(method, processor);
}
}
}
}
public boolean processPayment(PaymentRequest request) {
PaymentProcessor processor = processors.get(request.getMethod());
if (processor != null) {
return processor.processPayment(request);
}
throw new UnsupportedOperationException("No processor for method: " + request.getMethod());
}
}
Pattern One: Configuration-Driven Service Selection
Configuration-driven selection allows applications to choose implementations based on external settings. This pattern proves invaluable when different environments require different service behaviors.
public interface DatabaseConnector {
Connection getConnection();
String getConnectionType();
boolean isConfigured(Properties config);
}
public class MySQLConnector implements DatabaseConnector {
private Properties config;
public MySQLConnector() {
this.config = loadConfiguration();
}
@Override
public Connection getConnection() {
try {
return DriverManager.getConnection(
config.getProperty("mysql.url"),
config.getProperty("mysql.username"),
config.getProperty("mysql.password")
);
} catch (SQLException e) {
throw new RuntimeException("Failed to connect to MySQL", e);
}
}
@Override
public String getConnectionType() {
return "mysql";
}
@Override
public boolean isConfigured(Properties config) {
return config.containsKey("mysql.url") &&
config.containsKey("mysql.username");
}
private Properties loadConfiguration() {
Properties props = new Properties();
try (InputStream input = getClass().getResourceAsStream("/database.properties")) {
if (input != null) {
props.load(input);
}
} catch (IOException e) {
System.err.println("Failed to load database configuration");
}
return props;
}
}
public class PostgreSQLConnector implements DatabaseConnector {
private Properties config;
public PostgreSQLConnector() {
this.config = loadConfiguration();
}
@Override
public Connection getConnection() {
try {
return DriverManager.getConnection(
config.getProperty("postgresql.url"),
config.getProperty("postgresql.username"),
config.getProperty("postgresql.password")
);
} catch (SQLException e) {
throw new RuntimeException("Failed to connect to PostgreSQL", e);
}
}
@Override
public String getConnectionType() {
return "postgresql";
}
@Override
public boolean isConfigured(Properties config) {
return config.containsKey("postgresql.url") &&
config.containsKey("postgresql.username");
}
private Properties loadConfiguration() {
Properties props = new Properties();
try (InputStream input = getClass().getResourceAsStream("/database.properties")) {
if (input != null) {
props.load(input);
}
} catch (IOException e) {
System.err.println("Failed to load database configuration");
}
return props;
}
}
The service selector chooses implementations based on configuration availability:
public class DatabaseService {
private DatabaseConnector connector;
public DatabaseService() {
selectConnector();
}
private void selectConnector() {
Properties config = loadApplicationConfig();
String preferredType = config.getProperty("database.type", "auto");
ServiceLoader<DatabaseConnector> loader = ServiceLoader.load(DatabaseConnector.class);
if (!"auto".equals(preferredType)) {
// Use explicitly configured connector
for (DatabaseConnector candidate : loader) {
if (preferredType.equals(candidate.getConnectionType())) {
connector = candidate;
System.out.println("Selected configured connector: " + preferredType);
return;
}
}
}
// Auto-select based on available configuration
for (DatabaseConnector candidate : loader) {
if (candidate.isConfigured(config)) {
connector = candidate;
System.out.println("Auto-selected connector: " + candidate.getConnectionType());
return;
}
}
throw new RuntimeException("No suitable database connector found");
}
public Connection getConnection() {
return connector.getConnection();
}
private Properties loadApplicationConfig() {
Properties props = new Properties();
try (InputStream input = getClass().getResourceAsStream("/application.properties")) {
if (input != null) {
props.load(input);
}
} catch (IOException e) {
System.err.println("Failed to load application configuration");
}
return props;
}
}
Pattern Two: Multiple Provider Coexistence
Multiple providers can coexist within the same application, enabling feature flags, A/B testing, and gradual rollouts. This pattern supports experimentation without disrupting existing functionality.
public interface LoggingProvider {
void log(LogLevel level, String message, Object... args);
String getProviderName();
int getPriority();
boolean isEnabled();
}
public class ConsoleLoggingProvider implements LoggingProvider {
@Override
public void log(LogLevel level, String message, Object... args) {
String formatted = String.format("[%s] %s: %s",
System.currentTimeMillis(), level, String.format(message, args));
System.out.println(formatted);
}
@Override
public String getProviderName() {
return "console";
}
@Override
public int getPriority() {
return 100;
}
@Override
public boolean isEnabled() {
return true;
}
}
public class FileLoggingProvider implements LoggingProvider {
private final String logFile;
public FileLoggingProvider() {
this.logFile = System.getProperty("log.file", "application.log");
}
@Override
public void log(LogLevel level, String message, Object... args) {
String formatted = String.format("[%s] %s: %s%n",
System.currentTimeMillis(), level, String.format(message, args));
try (FileWriter writer = new FileWriter(logFile, true)) {
writer.write(formatted);
} catch (IOException e) {
System.err.println("Failed to write to log file: " + e.getMessage());
}
}
@Override
public String getProviderName() {
return "file";
}
@Override
public int getPriority() {
return 200;
}
@Override
public boolean isEnabled() {
return Boolean.parseBoolean(System.getProperty("file.logging.enabled", "true"));
}
}
public class ExperimentalCloudLoggingProvider implements LoggingProvider {
@Override
public void log(LogLevel level, String message, Object... args) {
// Simulate cloud logging
System.out.println("CLOUD: " + String.format(message, args));
}
@Override
public String getProviderName() {
return "experimental-cloud";
}
@Override
public int getPriority() {
return 300;
}
@Override
public boolean isEnabled() {
return Boolean.parseBoolean(System.getProperty("experimental.logging.enabled", "false"));
}
}
The logging service uses all enabled providers simultaneously:
public class LoggingService {
private final List<LoggingProvider> providers;
public LoggingService() {
providers = new ArrayList<>();
loadProviders();
}
private void loadProviders() {
ServiceLoader<LoggingProvider> loader = ServiceLoader.load(LoggingProvider.class);
for (LoggingProvider provider : loader) {
if (provider.isEnabled()) {
providers.add(provider);
System.out.println("Enabled logging provider: " + provider.getProviderName());
} else {
System.out.println("Disabled logging provider: " + provider.getProviderName());
}
}
// Sort by priority (higher priority first)
providers.sort((a, b) -> Integer.compare(b.getPriority(), a.getPriority()));
}
public void log(LogLevel level, String message, Object... args) {
for (LoggingProvider provider : providers) {
try {
provider.log(level, message, args);
} catch (Exception e) {
System.err.println("Logging provider failed: " + provider.getProviderName() +
" - " + e.getMessage());
}
}
}
public void info(String message, Object... args) {
log(LogLevel.INFO, message, args);
}
public void error(String message, Object... args) {
log(LogLevel.ERROR, message, args);
}
}
Pattern Three: Dynamic Service Reloading
Dynamic reloading enables applications to discover new implementations without restarts. This pattern supports hot-swapping of functionality in long-running applications.
public interface PluginService {
void execute(Map<String, Object> context);
String getPluginName();
String getVersion();
boolean isCompatible(String apiVersion);
}
public class EmailNotificationPlugin implements PluginService {
@Override
public void execute(Map<String, Object> context) {
String recipient = (String) context.get("email");
String message = (String) context.get("message");
System.out.println("Sending email to: " + recipient);
System.out.println("Message: " + message);
// Email sending logic here
}
@Override
public String getPluginName() {
return "email-notification";
}
@Override
public String getVersion() {
return "1.0.0";
}
@Override
public boolean isCompatible(String apiVersion) {
return "1.0".equals(apiVersion);
}
}
public class SlackNotificationPlugin implements PluginService {
@Override
public void execute(Map<String, Object> context) {
String channel = (String) context.get("channel");
String message = (String) context.get("message");
System.out.println("Posting to Slack channel: " + channel);
System.out.println("Message: " + message);
// Slack API integration here
}
@Override
public String getPluginName() {
return "slack-notification";
}
@Override
public String getVersion() {
return "2.1.0";
}
@Override
public boolean isCompatible(String apiVersion) {
return "1.0".equals(apiVersion) || "2.0".equals(apiVersion);
}
}
The plugin manager supports dynamic reloading:
public class PluginManager {
private final String apiVersion = "1.0";
private final Map<String, PluginService> plugins;
private final ScheduledExecutorService scheduler;
private volatile ServiceLoader<PluginService> serviceLoader;
public PluginManager() {
this.plugins = new ConcurrentHashMap<>();
this.scheduler = Executors.newScheduledThreadPool(1);
this.serviceLoader = ServiceLoader.load(PluginService.class);
loadPlugins();
startPeriodicReload();
}
private void loadPlugins() {
// Reload the service loader to discover new plugins
serviceLoader.reload();
Map<String, PluginService> newPlugins = new HashMap<>();
for (PluginService plugin : serviceLoader) {
if (plugin.isCompatible(apiVersion)) {
newPlugins.put(plugin.getPluginName(), plugin);
System.out.println("Loaded plugin: " + plugin.getPluginName() +
" v" + plugin.getVersion());
} else {
System.out.println("Incompatible plugin: " + plugin.getPluginName() +
" v" + plugin.getVersion());
}
}
// Update the plugin registry
plugins.clear();
plugins.putAll(newPlugins);
System.out.println("Total plugins loaded: " + plugins.size());
}
private void startPeriodicReload() {
scheduler.scheduleAtFixedRate(() -> {
try {
loadPlugins();
} catch (Exception e) {
System.err.println("Failed to reload plugins: " + e.getMessage());
}
}, 30, 30, TimeUnit.SECONDS);
}
public void executePlugin(String pluginName, Map<String, Object> context) {
PluginService plugin = plugins.get(pluginName);
if (plugin != null) {
try {
plugin.execute(context);
} catch (Exception e) {
System.err.println("Plugin execution failed: " + pluginName +
" - " + e.getMessage());
}
} else {
System.err.println("Plugin not found: " + pluginName);
}
}
public Set<String> getAvailablePlugins() {
return new HashSet<>(plugins.keySet());
}
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
Pattern Four: Extension Point Standardization
Extension points provide standardized interfaces for third-party developers. This pattern enables ecosystem development around your application core.
public interface DataTransformer {
TransformationResult transform(DataInput input);
String getTransformerType();
Set<String> getSupportedInputFormats();
Set<String> getSupportedOutputFormats();
TransformerMetadata getMetadata();
}
public class TransformerMetadata {
private final String name;
private final String description;
private final String version;
private final String author;
public TransformerMetadata(String name, String description, String version, String author) {
this.name = name;
this.description = description;
this.version = version;
this.author = author;
}
// Getters
public String getName() { return name; }
public String getDescription() { return description; }
public String getVersion() { return version; }
public String getAuthor() { return author; }
}
public class TransformationResult {
private final boolean success;
private final Object data;
private final String outputFormat;
private final String errorMessage;
private TransformationResult(boolean success, Object data, String outputFormat, String errorMessage) {
this.success = success;
this.data = data;
this.outputFormat = outputFormat;
this.errorMessage = errorMessage;
}
public static TransformationResult success(Object data, String outputFormat) {
return new TransformationResult(true, data, outputFormat, null);
}
public static TransformationResult failure(String errorMessage) {
return new TransformationResult(false, null, null, errorMessage);
}
// Getters
public boolean isSuccess() { return success; }
public Object getData() { return data; }
public String getOutputFormat() { return outputFormat; }
public String getErrorMessage() { return errorMessage; }
}
public class DataInput {
private final Object data;
private final String format;
private final Map<String, Object> parameters;
public DataInput(Object data, String format, Map<String, Object> parameters) {
this.data = data;
this.format = format;
this.parameters = parameters != null ? parameters : new HashMap<>();
}
// Getters
public Object getData() { return data; }
public String getFormat() { return format; }
public Map<String, Object> getParameters() { return parameters; }
}
Sample transformer implementations:
public class JsonToXmlTransformer implements DataTransformer {
@Override
public TransformationResult transform(DataInput input) {
try {
String jsonData = (String) input.getData();
// Simulate JSON to XML conversion
String xmlData = "<root>" + jsonData.replace("{", "<item>").replace("}", "</item>") + "</root>";
return TransformationResult.success(xmlData, "xml");
} catch (Exception e) {
return TransformationResult.failure("JSON to XML transformation failed: " + e.getMessage());
}
}
@Override
public String getTransformerType() {
return "json-to-xml";
}
@Override
public Set<String> getSupportedInputFormats() {
return Set.of("json");
}
@Override
public Set<String> getSupportedOutputFormats() {
return Set.of("xml");
}
@Override
public TransformerMetadata getMetadata() {
return new TransformerMetadata(
"JSON to XML Transformer",
"Converts JSON data structures to XML format",
"1.0.0",
"Internal Team"
);
}
}
public class CsvToJsonTransformer implements DataTransformer {
@Override
public TransformationResult transform(DataInput input) {
try {
String csvData = (String) input.getData();
// Simulate CSV to JSON conversion
String[] lines = csvData.split("\n");
StringBuilder json = new StringBuilder("[");
for (int i = 1; i < lines.length; i++) {
if (i > 1) json.append(",");
json.append("{\"data\":\"").append(lines[i]).append("\"}");
}
json.append("]");
return TransformationResult.success(json.toString(), "json");
} catch (Exception e) {
return TransformationResult.failure("CSV to JSON transformation failed: " + e.getMessage());
}
}
@Override
public String getTransformerType() {
return "csv-to-json";
}
@Override
public Set<String> getSupportedInputFormats() {
return Set.of("csv");
}
@Override
public Set<String> getSupportedOutputFormats() {
return Set.of("json");
}
@Override
public TransformerMetadata getMetadata() {
return new TransformerMetadata(
"CSV to JSON Transformer",
"Converts CSV files to JSON array format",
"1.2.1",
"Data Processing Team"
);
}
}
The transformation service manages registered transformers:
public class TransformationService {
private final Map<String, DataTransformer> transformers;
private final Map<String, Set<DataTransformer>> inputFormatIndex;
private final Map<String, Set<DataTransformer>> outputFormatIndex;
public TransformationService() {
this.transformers = new HashMap<>();
this.inputFormatIndex = new HashMap<>();
this.outputFormatIndex = new HashMap<>();
loadTransformers();
}
private void loadTransformers() {
ServiceLoader<DataTransformer> loader = ServiceLoader.load(DataTransformer.class);
for (DataTransformer transformer : loader) {
registerTransformer(transformer);
}
System.out.println("Loaded " + transformers.size() + " transformers");
}
private void registerTransformer(DataTransformer transformer) {
transformers.put(transformer.getTransformerType(), transformer);
// Index by input formats
for (String inputFormat : transformer.getSupportedInputFormats()) {
inputFormatIndex.computeIfAbsent(inputFormat, k -> new HashSet<>()).add(transformer);
}
// Index by output formats
for (String outputFormat : transformer.getSupportedOutputFormats()) {
outputFormatIndex.computeIfAbsent(outputFormat, k -> new HashSet<>()).add(transformer);
}
TransformerMetadata metadata = transformer.getMetadata();
System.out.println("Registered transformer: " + metadata.getName() +
" v" + metadata.getVersion() + " by " + metadata.getAuthor());
}
public TransformationResult transform(DataInput input, String targetFormat) {
Set<DataTransformer> candidates = inputFormatIndex.get(input.getFormat());
if (candidates == null || candidates.isEmpty()) {
return TransformationResult.failure("No transformer found for input format: " + input.getFormat());
}
for (DataTransformer transformer : candidates) {
if (transformer.getSupportedOutputFormats().contains(targetFormat)) {
return transformer.transform(input);
}
}
return TransformationResult.failure("No transformer found for conversion from " +
input.getFormat() + " to " + targetFormat);
}
public List<TransformerMetadata> getAvailableTransformers() {
return transformers.values().stream()
.map(DataTransformer::getMetadata)
.collect(Collectors.toList());
}
public Set<String> getSupportedInputFormats() {
return new HashSet<>(inputFormatIndex.keySet());
}
public Set<String> getSupportedOutputFormats() {
return new HashSet<>(outputFormatIndex.keySet());
}
}
Pattern Five: Contextual Service Resolution
Contextual resolution selects services based on runtime conditions and request characteristics. This pattern enables intelligent service routing and adaptive behavior.
public interface AuthenticationProvider {
AuthenticationResult authenticate(AuthenticationContext context);
boolean supports(AuthenticationContext context);
String getProviderName();
int getConfidenceLevel();
}
public class AuthenticationContext {
private final String username;
private final String password;
private final String clientIp;
private final String userAgent;
private final Map<String, Object> attributes;
public AuthenticationContext(String username, String password, String clientIp,
String userAgent, Map<String, Object> attributes) {
this.username = username;
this.password = password;
this.clientIp = clientIp;
this.userAgent = userAgent;
this.attributes = attributes != null ? attributes : new HashMap<>();
}
// Getters
public String getUsername() { return username; }
public String getPassword() { return password; }
public String getClientIp() { return clientIp; }
public String getUserAgent() { return userAgent; }
public Map<String, Object> getAttributes() { return attributes; }
}
public class AuthenticationResult {
private final boolean success;
private final String userId;
private final String message;
private final Map<String, Object> metadata;
private AuthenticationResult(boolean success, String userId, String message,
Map<String, Object> metadata) {
this.success = success;
this.userId = userId;
this.message = message;
this.metadata = metadata != null ? metadata : new HashMap<>();
}
public static AuthenticationResult success(String userId, Map<String, Object> metadata) {
return new AuthenticationResult(true, userId, "Authentication successful", metadata);
}
public static AuthenticationResult failure(String message) {
return new AuthenticationResult(false, null, message, null);
}
// Getters
public boolean isSuccess() { return success; }
public String getUserId() { return userId; }
public String getMessage() { return message; }
public Map<String, Object> getMetadata() { return metadata; }
}
Different authentication providers for various contexts:
public class DatabaseAuthenticationProvider implements AuthenticationProvider {
@Override
public AuthenticationResult authenticate(AuthenticationContext context) {
// Simulate database authentication
if ("admin".equals(context.getUsername()) && "password".equals(context.getPassword())) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("source", "database");
metadata.put("role", "administrator");
return AuthenticationResult.success("admin_user_123", metadata);
}
return AuthenticationResult.failure("Invalid credentials");
}
@Override
public boolean supports(AuthenticationContext context) {
// Support all standard username/password authentication
return context.getUsername() != null && context.getPassword() != null;
}
@Override
public String getProviderName() {
return "database";
}
@Override
public int getConfidenceLevel() {
return 50;
}
}
public class LdapAuthenticationProvider implements AuthenticationProvider {
@Override
public AuthenticationResult authenticate(AuthenticationContext context) {
// Simulate LDAP authentication
if (context.getUsername().contains("@company.com")) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("source", "ldap");
metadata.put("domain", "company.com");
return AuthenticationResult.success("ldap_user_456", metadata);
}
return AuthenticationResult.failure("LDAP authentication failed");
}
@Override
public boolean supports(AuthenticationContext context) {
// Support domain-based usernames
return context.getUsername() != null &&
context.getUsername().contains("@") &&
context.getPassword() != null;
}
@Override
public String getProviderName() {
return "ldap";
}
@Override
public int getConfidenceLevel() {
return 80;
}
}
public class ApiKeyAuthenticationProvider implements AuthenticationProvider {
@Override
public AuthenticationResult authenticate(AuthenticationContext context) {
String apiKey = (String) context.getAttributes().get("api_key");
if ("secret_api_key_123".equals(apiKey)) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("source", "api_key");
metadata.put("key_type", "service");
return AuthenticationResult.success("api_user_789", metadata);
}
return AuthenticationResult.failure("Invalid API key");
}
@Override
public boolean supports(AuthenticationContext context) {
// Support API key authentication
return context.getAttributes().containsKey("api_key");
}
@Override
public String getProviderName() {
return "api_key";
}
@Override
public int getConfidenceLevel() {
return 90;
}
}
The authentication service uses contextual resolution:
java
public class AuthenticationService {
private final List<AuthenticationProvider> providers;
public AuthenticationService() {
this.providers = new ArrayList<>();
loadProviders();
}
private void loadProviders() {
ServiceLoader<AuthenticationProvider> loader = ServiceLoader.load(AuthenticationProvider.class);
for (AuthenticationProvider provider : loader) {
providers.add(provider);
System.out.println("Loaded authentication provider: " + provider.getProviderName() +
" (confidence: " + provider.getConfidenceLevel() + ")");
}
// Sort by confidence level (highest first)
providers.sort((a, b) -> Integer.compare(b.getConfidenceLevel(), a.getConfidenceLevel()));
}
public AuthenticationResult authenticate(AuthenticationContext context) {
List<AuthenticationProvider> supportedProviders = providers.stream()
.filter(provider -> provider.supports(context))
.collect(Collectors.toList());
if (supportedProviders.isEmpty()) {
return AuthenticationResult.failure("No suitable authentication provider found");
}
// Try providers in order of confidence
for (AuthenticationProvider provider : supportedProviders) {
try {
System.out.println("Attempting authentication with provider: " + provider.getProviderName());
AuthenticationResult result = provider.authenticate(context);
if (result.isSuccess()) {
System.out.println("Authentication successful with provider: " +
---
## 101 Books
**101 Books** is an AI-driven publishing company co-founded by author **Aarav Joshi**. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as **$4**—making quality knowledge accessible to everyone.
Check out our book **[Golang Clean Code](https://www.amazon.com/dp/B0DQQF9K3Z)** available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for **Aarav Joshi** to find more of our titles. Use the provided link to enjoy **special discounts**!
## Our Creations
Be sure to check out our creations:
**[Investor Central](https://www.investorcentral.co.uk/)** | **[Investor Central Spanish](https://spanish.investorcentral.co.uk/)** | **[Investor Central German](https://german.investorcentral.co.uk/)** | **[Smart Living](https://smartliving.investorcentral.co.uk/)** | **[Epochs & Echoes](https://epochsandechoes.com/)** | **[Puzzling Mysteries](https://www.puzzlingmysteries.com/)** | **[Hindutva](http://hindutva.epochsandechoes.com/)** | **[Elite Dev](https://elitedev.in/)** | **[JS Schools](https://jsschools.com/)**
---
### We are on Medium
**[Tech Koala Insights](https://techkoalainsights.com/)** | **[Epochs & Echoes World](https://world.epochsandechoes.com/)** | **[Investor Central Medium](https://medium.investorcentral.co.uk/)** | **[Puzzling Mysteries Medium](https://medium.com/puzzling-mysteries)** | **[Science & Epochs Medium](https://science.epochsandechoes.com/)** | **[Modern Hindutva](https://modernhindutva.substack.com/)**
Top comments (0)