refactor: Migrate from monolithic to modular architecture
### **Service-Implementation** - [ ] **Tag 1**: Members-Service REST-API implementieren - [ ] **Tag 2**: Database-Migrations und Repository-Layer - [ ] **Tag 3**: Event-Publishing nach Kafka aktivieren - [ ] **Tag 4**: Horses-Service analog implementieren - [ ] **Tag 5**: Integration-Tests für beide Services - [ ] **Tag 6-7**: Events-Service und Masterdata-Service
This commit is contained in:
+3
-2
@@ -1,6 +1,7 @@
|
||||
package at.mocode.infrastructure.gateway.migrations
|
||||
|
||||
import at.mocode.core.utils.database.Migration
|
||||
import at.mocode.members.infrastructure.persistence.MemberTable
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.date
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||
@@ -11,8 +12,8 @@ import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp
|
||||
*/
|
||||
class MemberManagementTablesCreation : Migration(2, "Create member management tables") {
|
||||
override fun up() {
|
||||
// Person-Tabelle
|
||||
SchemaUtils.create(PersonTable)
|
||||
// Member-Tabelle
|
||||
SchemaUtils.create(MemberTable)
|
||||
|
||||
// Verein-Tabelle
|
||||
SchemaUtils.create(VereinTable)
|
||||
|
||||
+80
-27
@@ -2,10 +2,18 @@ package at.mocode.infrastructure.gateway.routing
|
||||
|
||||
import at.mocode.infrastructure.gateway.discovery.ServiceDiscovery
|
||||
import at.mocode.core.utils.config.AppConfig
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.util.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
@@ -61,40 +69,56 @@ fun Routing.serviceRoutes() {
|
||||
}
|
||||
} else null
|
||||
|
||||
// Define service routes
|
||||
// Define service routes with all HTTP methods
|
||||
// Master Data Service Routes
|
||||
route("/api/masterdata") {
|
||||
handle {
|
||||
handleServiceRequest(call, "master-data", serviceDiscovery)
|
||||
}
|
||||
get("{...}") { handleServiceRequest(call, "master-data", serviceDiscovery) }
|
||||
post("{...}") { handleServiceRequest(call, "master-data", serviceDiscovery) }
|
||||
put("{...}") { handleServiceRequest(call, "master-data", serviceDiscovery) }
|
||||
delete("{...}") { handleServiceRequest(call, "master-data", serviceDiscovery) }
|
||||
patch("{...}") { handleServiceRequest(call, "master-data", serviceDiscovery) }
|
||||
}
|
||||
|
||||
// Horse Registry Service Routes
|
||||
route("/api/horses") {
|
||||
handle {
|
||||
handleServiceRequest(call, "horse-registry", serviceDiscovery)
|
||||
}
|
||||
get("{...}") { handleServiceRequest(call, "horse-registry", serviceDiscovery) }
|
||||
post("{...}") { handleServiceRequest(call, "horse-registry", serviceDiscovery) }
|
||||
put("{...}") { handleServiceRequest(call, "horse-registry", serviceDiscovery) }
|
||||
delete("{...}") { handleServiceRequest(call, "horse-registry", serviceDiscovery) }
|
||||
patch("{...}") { handleServiceRequest(call, "horse-registry", serviceDiscovery) }
|
||||
}
|
||||
|
||||
// Event Management Service Routes
|
||||
route("/api/events") {
|
||||
handle {
|
||||
handleServiceRequest(call, "event-management", serviceDiscovery)
|
||||
}
|
||||
get("{...}") { handleServiceRequest(call, "event-management", serviceDiscovery) }
|
||||
post("{...}") { handleServiceRequest(call, "event-management", serviceDiscovery) }
|
||||
put("{...}") { handleServiceRequest(call, "event-management", serviceDiscovery) }
|
||||
delete("{...}") { handleServiceRequest(call, "event-management", serviceDiscovery) }
|
||||
patch("{...}") { handleServiceRequest(call, "event-management", serviceDiscovery) }
|
||||
}
|
||||
|
||||
// Member Management Service Routes
|
||||
route("/api/members") {
|
||||
handle {
|
||||
handleServiceRequest(call, "member-management", serviceDiscovery)
|
||||
}
|
||||
get("{...}") { handleServiceRequest(call, "member-management", serviceDiscovery) }
|
||||
post("{...}") { handleServiceRequest(call, "member-management", serviceDiscovery) }
|
||||
put("{...}") { handleServiceRequest(call, "member-management", serviceDiscovery) }
|
||||
delete("{...}") { handleServiceRequest(call, "member-management", serviceDiscovery) }
|
||||
patch("{...}") { handleServiceRequest(call, "member-management", serviceDiscovery) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP client for forwarding requests to backend services
|
||||
*/
|
||||
private val httpClient = HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a service request by discovering the service and forwarding the request.
|
||||
* This is a simplified implementation that just returns service information.
|
||||
* In a production environment, this would forward the request to the service.
|
||||
* This implementation forwards the complete HTTP request to the backend service.
|
||||
*/
|
||||
private suspend fun handleServiceRequest(
|
||||
call: ApplicationCall,
|
||||
@@ -125,18 +149,47 @@ private suspend fun handleServiceRequest(
|
||||
return
|
||||
}
|
||||
|
||||
// Respond with service information
|
||||
val successResponse = ServiceSuccessResponse(
|
||||
message = "Service discovery working",
|
||||
service = serviceName,
|
||||
instance = ServiceInstanceInfo(
|
||||
id = serviceInstance.id,
|
||||
name = serviceInstance.name,
|
||||
host = serviceInstance.host,
|
||||
port = serviceInstance.port
|
||||
)
|
||||
)
|
||||
call.respond(HttpStatusCode.OK, successResponse)
|
||||
// Build target URL
|
||||
val targetUrl = "http://${serviceInstance.host}:${serviceInstance.port}${call.request.uri}"
|
||||
|
||||
// Forward the request to the backend service
|
||||
val response = httpClient.request(targetUrl) {
|
||||
method = call.request.httpMethod
|
||||
|
||||
// Copy all headers except Host and Content-Length (handled automatically)
|
||||
call.request.headers.forEach { name, values ->
|
||||
if (name.lowercase() !in listOf("host", "content-length")) {
|
||||
values.forEach { value ->
|
||||
header(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy request body if present
|
||||
if (call.request.httpMethod in listOf(HttpMethod.Post, HttpMethod.Put, HttpMethod.Patch)) {
|
||||
val requestBody = call.receiveText()
|
||||
if (requestBody.isNotEmpty()) {
|
||||
setBody(requestBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forward the response back to the client
|
||||
call.response.status(response.status)
|
||||
|
||||
// Copy response headers
|
||||
response.headers.forEach { name, values ->
|
||||
if (name.lowercase() !in listOf("content-length", "transfer-encoding")) {
|
||||
values.forEach { value ->
|
||||
call.response.header(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy response body
|
||||
val responseBody = response.bodyAsText()
|
||||
call.respondText(responseBody, response.contentType())
|
||||
|
||||
} catch (e: Exception) {
|
||||
val errorResponse = ServiceErrorResponse(
|
||||
error = "Error routing request to service $serviceName: ${e.message}",
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package at.mocode.infrastructure.messaging.client
|
||||
|
||||
/**
|
||||
* Interface for publishing domain events to message broker.
|
||||
*/
|
||||
interface EventPublisher {
|
||||
|
||||
/**
|
||||
* Publishes an event to the specified topic.
|
||||
*
|
||||
* @param topic The topic to publish to
|
||||
* @param key The message key (optional)
|
||||
* @param event The event to publish
|
||||
*/
|
||||
suspend fun publishEvent(topic: String, key: String? = null, event: Any)
|
||||
|
||||
/**
|
||||
* Publishes multiple events to the specified topic.
|
||||
*
|
||||
* @param topic The topic to publish to
|
||||
* @param events The events to publish with their keys
|
||||
*/
|
||||
suspend fun publishEvents(topic: String, events: List<Pair<String?, Any>>)
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package at.mocode.infrastructure.messaging.client
|
||||
|
||||
import kotlinx.coroutines.future.await
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.kafka.core.KafkaTemplate
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
/**
|
||||
* Kafka implementation of EventPublisher.
|
||||
*/
|
||||
@Component
|
||||
class KafkaEventPublisher(
|
||||
private val kafkaTemplate: KafkaTemplate<String, Any>
|
||||
) : EventPublisher {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(KafkaEventPublisher::class.java)
|
||||
|
||||
override suspend fun publishEvent(topic: String, key: String?, event: Any) {
|
||||
try {
|
||||
logger.debug("Publishing event to topic '{}' with key '{}'", topic, key)
|
||||
|
||||
val sendResult = if (key != null) {
|
||||
kafkaTemplate.send(topic, key, event).get()
|
||||
} else {
|
||||
kafkaTemplate.send(topic, event).get()
|
||||
}
|
||||
|
||||
logger.info("Successfully published event to topic '{}' with key '{}'", topic, key)
|
||||
} catch (exception: Exception) {
|
||||
logger.error("Failed to publish event to topic '{}' with key '{}'", topic, key, exception)
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun publishEvents(topic: String, events: List<Pair<String?, Any>>) {
|
||||
try {
|
||||
logger.debug("Publishing {} events to topic '{}'", events.size, topic)
|
||||
|
||||
events.forEach { (key, event) ->
|
||||
publishEvent(topic, key, event)
|
||||
}
|
||||
|
||||
logger.info("Successfully published {} events to topic '{}'", events.size, topic)
|
||||
} catch (exception: Exception) {
|
||||
logger.error("Failed to publish events to topic '{}'", topic, exception)
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package at.mocode.infrastructure.messaging.config
|
||||
|
||||
import org.apache.kafka.clients.producer.ProducerConfig
|
||||
import org.apache.kafka.common.serialization.StringSerializer
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.kafka.core.DefaultKafkaProducerFactory
|
||||
import org.springframework.kafka.core.KafkaTemplate
|
||||
import org.springframework.kafka.core.ProducerFactory
|
||||
import org.springframework.kafka.support.serializer.JsonSerializer
|
||||
|
||||
/**
|
||||
* Kafka configuration for event publishing.
|
||||
*/
|
||||
@Configuration
|
||||
class KafkaConfig {
|
||||
|
||||
@Value("\${spring.kafka.bootstrap-servers:localhost:9092}")
|
||||
private lateinit var bootstrapServers: String
|
||||
|
||||
@Bean
|
||||
fun producerFactory(): ProducerFactory<String, Any> {
|
||||
val configProps = mapOf(
|
||||
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
|
||||
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
|
||||
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java,
|
||||
ProducerConfig.ACKS_CONFIG to "all",
|
||||
ProducerConfig.RETRIES_CONFIG to 3,
|
||||
ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG to true,
|
||||
ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION to 1
|
||||
)
|
||||
return DefaultKafkaProducerFactory(configProps)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kafkaTemplate(): KafkaTemplate<String, Any> {
|
||||
return KafkaTemplate(producerFactory())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user