Making one service dependent on another service
When developing the chat room functionality, I found that the ChatRoomService was heavily utilizing invitation-related functions, which seemed out of place within the ChatRoomService. Initially, I moved these functions into the InvitationRepository, but upon further reflection, I realized that there’s no rule that services can’t depend on other services.
It’s not necessary to confine logic to repositories only, so I decided to refactor the code by separating the logic into a dedicated InvitationService.
Original Structure:
@Service
class ChatRoomService(
private val chatRoomRepository: ChatRoomRepository,
private val invitationRepository: InvitationRepository,
) {
fun getChatRoomInvitation(
chatRoomId: String,
userId: String,
): InvitationResponse {
verifyChatRoomAccess(chatRoomId, userId)
val code = invitationRepository.getInvitationCode(userId, chatRoomId)
return InvitationResponse(code)
}
}
@Component
class InvitationRepository(
private val redisTemplate: RedisTemplate<String, String>,
private val propertyConfig: PropertyConfig,
) {
fun getInvitationCode(
userId: String,
chatRoomId: String,
): String {
val key = generateKey(userId, chatRoomId)
var value = redisTemplate.opsForValue().get(key)
if (value == null) {
value = setInvitation(key, chatRoomId)
}
return generateCode(key, value)
}
fun setInvitation(
key: String,
chatRoomId: String,
): String {
val value = generateValue()
val ops: ValueOperations<String, String> = redisTemplate.opsForValue()
ops.set(key, value, propertyConfig.getExpiration())
return value
}
fun getExpiration(
userId: String,
chatRoomId: String,
): Long {
val key = generateKey(userId, chatRoomId)
val expiration = redisTemplate.getExpire(key)
return expiration
}
fun generateCode(
key: String,
value: String,
): String {
val combine = "$key,$value"
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(combine.toByteArray(StandardCharsets.UTF_8))
return Base64.getEncoder().encodeToString(hash)
}
private fun generateKey(
userId: String,
chatRoomId: String,
): String {
return "$userId:$chatRoomId"
}
private fun generateValue(): String {
return UUID.randomUUID().toString()
}
}
@Repository
interface ChatRoomRepository : MongoRepository<ChatRoom, String> {
fun existsByIdAndMembersContaining(
roomId: String,
userId: String,
): Boolean
}
✨Improvement: Separation of Invitation Service Logic
Initially, it felt out of place to have invitation-related logic within the ChatRoomService, so I moved it to the InvitationRepository. However, after reconsideration, I realized that there’s no rule against one service depending on another. It’s not necessary to limit logic to repositories alone, so I decided to refactor and move the logic into a dedicated InvitationService.
Improved Structure:
@Service
class ChatRoomService(
private val chatRoomRepository: ChatRoomRepository,
private val invitationService: InvitationService,
) {
fun getChatRoomInvitation(
chatRoomId: String,
userId: String,
): InvitationResponse {
verifyChatRoomAccess(chatRoomId, userId)
var code = invitationService.getInvitation(userId, chatRoomId)
if (code == null) {
code = invitationService.setInvitation(userId, chatRoomId)
}
val encodedCode = invitationService.generateKeyAndCode(userId, chatRoomId, code)
return InvitationResponse(encodedCode)
}
}
@Service
class InvitationService(
private val invitationRepository: InvitationRepository,
private val chatRoomProperty: ChatRoomProperty,
) {
fun getInvitation(
userId: String,
chatRoomId: String,
): String? {
val key = generateKey(userId, chatRoomId)
return invitationRepository.getValue(key)
}
fun setInvitation(
userId: String,
chatRoomId: String,
): String {
val key = generateKey(userId, chatRoomId)
val value = generateValue()
invitationRepository.setValueAndExpiration(key, value, chatRoomProperty.getExpiration())
return value
}
fun generateKeyAndCode(
userId: String,
chatRoomId: String,
code: String,
): String {
val key = generateKey(userId, chatRoomId)
return generateCode(key, code)
}
private fun generateCode(
key: String,
value: String,
): String {
val combinedString = "$key,$value"
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(combinedString.toByteArray(StandardCharsets.UTF_8))
return Base64.getEncoder().encodeToString(hash)
}
private fun generateKey(
userId: String,
chatRoomId: String,
): String {
return "$userId:$chatRoomId"
}
private fun generateValue(): String {
return UUID.randomUUID().toString()
}
}
@Component
class InvitationRepository(
private val redisTemplate: RedisTemplate<String, String>,
) {
fun getValue(key: String): String? {
return redisTemplate.opsForValue().get(key)
}
fun setValueAndExpiration(
key: String,
value: String,
expiration: Duration,
) {
val ops: ValueOperations<String, String> = redisTemplate.opsForValue()
ops.set(key, value, expiration)
}
fun getExpiration(key: String): Long {
return redisTemplate.getExpire(key)
}
}
This new structure separates the logic into three distinct layers: ChatRoomService, InvitationService, and InvitationRepository, making the code more modular and maintainable.