-
I am developing logic to handle REST API responses for different tenants (e.g., tenants A, B, and C) using a unified API approach. Each tenant has a specific response format based on a general response. For example, Tenant A will return General, and Tenant B will return General, and I plan to apply this approach to other APIs as well.
-
I intend to manage these responses within a manager class and utilize a factory method to ensure that the appropriate function from the relevant manager is called for each tenant. However, I am concerned that as the number of tenants increases, the interface might become overloaded with functions, making it challenging to manage.
-
Additionally, if there are some APIs might affect only specific tenants (e.g., Tenant A), and if I include these in the general interface, all inherited interfaces for other tenants would need to accommodate these new methods, which could lead to complications.
Here’s my current flow
- The MetaFactory class is used to determine which MetaManager should be called based on the tenant’s subdomain:
class MetaFactory(
private val defaultMetaManager: DefaultMetaManager,
private val tenantAMetaManager: TenantAMetaManager,
private val tenantBMetaManager: TenantBMetaManager,
) {
fun getMetaManager(subdomain: Subdomain): MetaManager {
return when (subdomain) {
TenantA -> TenantAMetaManager
TenantB -> TenantBMetaManager
else -> defaultMetaManager
}
}
}
- The MetaManager interface defines all required functions that a controller can call, including both general and tenant-specific responses:
interface MetaManager {
// All tenant requires these responses
fun getDefaultResponse1() : GeneralResponse1<Meta> // api 1 call to
fun getDefaultResponse2() : GeneralResponse2<Meta> // api 2 call to
// Specific responses for tenant A
fun getTenantAResponse1() : GeneralResponse1<Meta> // a specific api from tenant A call to
fun getTenantAResponse2() : GeneralResponse2<Meta> // a specific api from tenant A call to
// Specific responses for tenant B
fun getTenantBResponse1() : GeneralResponse2<Meta> // a specific api from tenant B call to
}
- Define response data classes with a Meta interface to handle specific responses:
data class GeneralResponse1<E: Meta?(
val id: UUID,
val name: String
val meta: E?
)
data class TenantAResponse1(
val score: Int?
): Meta
- Implement the MetaManager interface for each tenant, defining how each function returns specific responses:
class TenantAMetaManager(): MetaManager() {
override fun getDefaultResponse1() : GeneralResponse1<Meta> // todo
override fun getDefaultResponse2() : GeneralResponse2<Meta> // todo
override fun getTenantAResponse1() : GeneralResponse1<Meta> // todo
override fun getTenantAResponse2() : GeneralResponse2<Meta> {
val score = getScore() // random function to resolve the score
val meta = TenantAResponse1(
score = score
)
return GeneralResponse2(meta)
}
override fun getTenantBResponse1() : GeneralResponse2<Meta> // todo
}
- The controller then can determine which response to return by checking the subdomain and using the factory method.
@Get("/")
fun A (request: Request, subdomain: Subdomain): GeneralResponse1<Meta> {
return metaFactory(subdomain).getMetaManager()
}
There are some concerns as I said:
- Inconvenience: The current design requires defining a comprehensive interface with many functions, which can become cumbersome as the number of tenants grows.
- Specific APIs: When new APIs are introduced that affect only certain tenants, they need to be added to the general interface, which may result in unnecessary complexity for other tenants.
kryptonite is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.