Repository: cjbooms/fabrikt
Pull request #37 (2021-04-28 14:47:45)

Issue #30: Generate Controller Interfaces instead of implementations
Issue #30: Generate Controller Interfaces instead of implementations

cjbooms: 👍 

Patch:
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
index 366f4d9..c244a3c 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
@@ -52,7 +52,3 @@ enum class ModelCodeGenOptionType(val description: String) {
 
     override fun toString() = "`${super.toString()}` - $description"
 }
-
-data class CodeGenOptions(
-    val clientOptions: Set<ClientCodeGenOptionType> = emptySet()
-)
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
index 07e1383..fb88d82 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
@@ -7,10 +7,9 @@ import com.cjbooms.fabrikt.cli.CodeGenerationType.QUARKUS_REFLECTION_CONFIG
 import com.cjbooms.fabrikt.configurations.Packages
 import com.cjbooms.fabrikt.generators.MutableSettings
 import com.cjbooms.fabrikt.generators.client.OkHttpClientGenerator
-import com.cjbooms.fabrikt.generators.controller.SpringControllerGenerator
+import com.cjbooms.fabrikt.generators.controller.SpringControllerInterfaceGenerator
 import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator
 import com.cjbooms.fabrikt.generators.model.QuarkusReflectionModelGenerator
-import com.cjbooms.fabrikt.generators.service.SpringServiceInterfaceGenerator
 import com.cjbooms.fabrikt.model.Clients
 import com.cjbooms.fabrikt.model.Controllers
 import com.cjbooms.fabrikt.model.GeneratedFile
@@ -18,7 +17,6 @@ import com.cjbooms.fabrikt.model.KotlinSourceSet
 import com.cjbooms.fabrikt.model.Models
 import com.cjbooms.fabrikt.model.ResourceFile
 import com.cjbooms.fabrikt.model.ResourceSourceSet
-import com.cjbooms.fabrikt.model.Services
 import com.cjbooms.fabrikt.model.SourceApi
 import com.squareup.kotlinpoet.FileSpec
 
@@ -29,18 +27,15 @@ class CodeGenerator(private val packages: Packages, private val sourceApi: Sourc
     private fun generateCode(generationType: CodeGenerationType): Collection<GeneratedFile> =
         when (generationType) {
             CLIENT -> generateClient()
-            CONTROLLERS -> generateControllers()
+            CONTROLLERS -> generateControllerInterfaces()
             HTTP_MODELS -> generateModels()
             QUARKUS_REFLECTION_CONFIG -> generateQuarkusReflectionResource()
         }
 
     private fun generateModels(): Collection<GeneratedFile> = sourceSet(models().files)
 
-    private fun generateServiceInterfaces(): Collection<GeneratedFile> =
-        sourceSet(services().files).plus(sourceSet(models().files))
-
-    private fun generateControllers(): Collection<GeneratedFile> =
-        sourceSet(controllers().files).plus(generateServiceInterfaces())
+    private fun generateControllerInterfaces(): Collection<GeneratedFile> =
+        sourceSet(controllers().files).plus(sourceSet(models().files))
 
     private fun generateClient(): Collection<GeneratedFile> {
         val lib = OkHttpClientGenerator(packages, sourceApi).generateLibrary(MutableSettings.clientOptions())
@@ -59,11 +54,8 @@ class CodeGenerator(private val packages: Packages, private val sourceApi: Sourc
     private fun resources(models: Models): List<ResourceFile> =
         listOfNotNull(QuarkusReflectionModelGenerator(models, MutableSettings.generationTypes()).generate())
 
-    private fun services(): Services =
-        SpringServiceInterfaceGenerator(packages, sourceApi, models().models).generate()
-
     private fun controllers(): Controllers =
-        SpringControllerGenerator(packages, sourceApi, services().services, models().models).generate()
+        SpringControllerInterfaceGenerator(packages, sourceApi, models().models).generate()
 
     private fun client(): Clients =
         OkHttpClientGenerator(packages, sourceApi).generate(MutableSettings.clientOptions())
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/configurations/Packages.kt b/src/main/kotlin/com/cjbooms/fabrikt/configurations/Packages.kt
index 9d95517..63c307e 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/configurations/Packages.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/configurations/Packages.kt
@@ -3,11 +3,9 @@ package com.cjbooms.fabrikt.configurations
 import com.cjbooms.fabrikt.model.Destinations.clientPackage
 import com.cjbooms.fabrikt.model.Destinations.controllersPackage
 import com.cjbooms.fabrikt.model.Destinations.modelsPackage
-import com.cjbooms.fabrikt.model.Destinations.servicesPackage
 
 class Packages(val base: String) {
     val models: String = modelsPackage(base)
     val client: String = clientPackage(base)
-    val services: String = servicesPackage(base)
     val controllers: String = controllersPackage(base)
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/ControllerGeneratorUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/ControllerGeneratorUtils.kt
index 4539d01..ae1e59f 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/ControllerGeneratorUtils.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/ControllerGeneratorUtils.kt
@@ -19,17 +19,17 @@ object ControllerGeneratorUtils {
      * Pulling out the first response. This assumes first is happy path
      * may need to revisit if we want to have conditional responses
      */
-    fun Operation.happyPathResponse(basePackage: String): Pair<Int, TypeName> {
+    fun Operation.happyPathResponse(basePackage: String): TypeName {
         // Map of response code to nullable name of schema
         val responseDetails = happyPathResponseObject()
-        return responseDetails.first to (responseDetails
+        return responseDetails
             .second
             .contentMediaTypes
             .map { it.value?.schema }
             .filterNotNull()
             .firstOrNull()
             ?.let { toModelType(basePackage, KotlinTypeInfo.from(it)) }
-            ?: Unit::class.asTypeName())
+            ?: Unit::class.asTypeName()
     }
 
     fun Operation.happyPathResponseObject(): Pair<Int, Response> {
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerCodeBlocks.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerCodeBlocks.kt
deleted file mode 100644
index 8ff9813..0000000
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerCodeBlocks.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-package com.cjbooms.fabrikt.generators.controller
-
-import com.cjbooms.fabrikt.generators.controller.metadata.SpringImports
-import com.cjbooms.fabrikt.model.IncomingParameter
-import com.squareup.kotlinpoet.CodeBlock
-
-data class SimpleCodeBlock(val body: String, val args: List<Any> = emptyList())
-
-fun SimpleCodeBlock.toCodeBlock() = CodeBlock.of(body, *args.toTypedArray())
-
-object ControllerCodeBlocks {
-
-    fun postWithResponsebody(serviceFuncName: String, parameters: List<IncomingParameter>): SimpleCodeBlock =
-        SimpleCodeBlock(
-            """return ResponseEntity
-                .ok(${serviceCallLine(serviceFuncName, parameters)}.second!!)
-            """
-        )
-
-    fun postWithLocationResponse(serviceFuncName: String, parameters: List<IncomingParameter>, withBody: Boolean): SimpleCodeBlock {
-        val conditionalObjectResponse = "if (svcResp.second != null) response.body(svcResp.second)"
-        return SimpleCodeBlock(
-            """val svcResp = ${serviceCallLine(serviceFuncName, parameters)}
-                val response = %T.created(svcResp.first) ${if (withBody) "\n$conditionalObjectResponse" else "" }
-                return response.build()
-               """.trimIndent(),
-            listOf(
-                SpringImports.RESPONSE_ENTITY
-            )
-        )
-    }
-
-    fun getSingleResource(serviceFuncName: String, parameters: List<IncomingParameter>): SimpleCodeBlock =
-        SimpleCodeBlock(
-            """val svcResp = ${serviceCallLine(serviceFuncName, parameters)}
-               return %T
-                   .ok()
-                   .body(svcResp)
-               """.trimIndent(),
-            listOf(
-                SpringImports.RESPONSE_ENTITY
-            )
-        )
-
-    fun putWithNoContent(serviceFuncName: String, parameters: List<IncomingParameter>): SimpleCodeBlock {
-        return SimpleCodeBlock(
-            """${serviceCallLine(serviceFuncName, parameters)}
-               return ResponseEntity.noContent().build()
-            """
-        )
-    }
-
-    fun deleteWithNoContent(serviceFuncName: String, parameters: List<IncomingParameter>): SimpleCodeBlock {
-        return SimpleCodeBlock(
-            """${serviceCallLine(serviceFuncName, parameters)}
-               return ResponseEntity.noContent().build()
-            """
-        )
-    }
-
-    fun genericBody(serviceFuncName: String, parameters: List<IncomingParameter>, responseCode: Int): SimpleCodeBlock {
-        return SimpleCodeBlock(
-            """${serviceCallLine(serviceFuncName, parameters)}
-            return ResponseEntity.status($responseCode).build()
-            """, listOf(SpringImports.URI_BUILDER)
-        )
-    }
-
-    private fun serviceCallLine(serviceFuncName: String, parameters: List<IncomingParameter>): String =
-        "service.$serviceFuncName(${parameters.joinToString(", ") { it.name }})"
-}
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerInterfaceGenerator.kt
similarity index 63%
rename from src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerGenerator.kt
rename to src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerInterfaceGenerator.kt
index dd41771..bbc0259 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerInterfaceGenerator.kt
@@ -2,20 +2,12 @@ package com.cjbooms.fabrikt.generators.controller
 
 import com.cjbooms.fabrikt.configurations.Packages
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toKdoc
-import com.cjbooms.fabrikt.generators.controller.ControllerCodeBlocks.deleteWithNoContent
-import com.cjbooms.fabrikt.generators.controller.ControllerCodeBlocks.genericBody
-import com.cjbooms.fabrikt.generators.controller.ControllerCodeBlocks.getSingleResource
-import com.cjbooms.fabrikt.generators.controller.ControllerCodeBlocks.postWithLocationResponse
-import com.cjbooms.fabrikt.generators.controller.ControllerCodeBlocks.postWithResponsebody
-import com.cjbooms.fabrikt.generators.controller.ControllerCodeBlocks.putWithNoContent
 import com.cjbooms.fabrikt.generators.controller.ControllerGeneratorUtils.controllerName
 import com.cjbooms.fabrikt.generators.controller.ControllerGeneratorUtils.happyPathResponse
 import com.cjbooms.fabrikt.generators.controller.ControllerGeneratorUtils.methodName
 import com.cjbooms.fabrikt.generators.controller.ControllerGeneratorUtils.toIncomingParameters
 import com.cjbooms.fabrikt.generators.controller.metadata.JavaXAnnotations
 import com.cjbooms.fabrikt.generators.controller.metadata.SpringAnnotations
-import com.cjbooms.fabrikt.generators.controller.metadata.SpringImports
-import com.cjbooms.fabrikt.generators.service.SpringServiceInterfaceGenerator
 import com.cjbooms.fabrikt.model.BodyParameter
 import com.cjbooms.fabrikt.model.ControllerType
 import com.cjbooms.fabrikt.model.Controllers
@@ -24,7 +16,6 @@ import com.cjbooms.fabrikt.model.ModelType
 import com.cjbooms.fabrikt.model.PathParam
 import com.cjbooms.fabrikt.model.QueryParam
 import com.cjbooms.fabrikt.model.RequestParameter
-import com.cjbooms.fabrikt.model.ServiceType
 import com.cjbooms.fabrikt.model.SourceApi
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.basePath
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSingleResource
@@ -32,15 +23,13 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.routeToPaths
 import com.reprezen.kaizen.oasparser.model3.Operation
 import com.reprezen.kaizen.oasparser.model3.Path
 import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.ParameterSpec
-import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
-import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeSpec
 
-class SpringControllerGenerator(
+class SpringControllerInterfaceGenerator(
     private val packages: Packages,
     private val api: SourceApi,
-    private val services: Collection<ServiceType>,
     private val models: Collection<ModelType>
 ) {
 
@@ -52,11 +41,8 @@ class SpringControllerGenerator(
         )
 
     private fun buildController(resourceName: String, paths: Collection<Path>): ControllerType {
-        val service = services.first { it.spec.name == "$resourceName${ServiceType.SUFFIX}" }
-
         val typeBuilder: TypeSpec.Builder = controllerBuilder(
             className = controllerName(resourceName),
-            service = service,
             basePath = api.openApi3.basePath()
         )
 
@@ -64,15 +50,10 @@ class SpringControllerGenerator(
             path.operations
                 .filter { it.key.toUpperCase() != "HEAD" }
                 .map { op ->
-                    val serviceFunction = SpringServiceInterfaceGenerator.toMethod(packages, op.key, op.value, path, models, api)
-                            ?.let { method ->
-                                service.spec.funSpecs.firstOrNull { it.name == method.name }
-                            }
                     buildFunction(
                         op.value,
                         op.key,
-                        path.pathString,
-                        serviceFunction
+                        path.pathString
                     )
                 }
         }.forEach { typeBuilder.addFunction(it) }
@@ -85,63 +66,29 @@ class SpringControllerGenerator(
 
     private fun controllerBuilder(
         className: String,
-        service: ServiceType,
         basePath: String
     ) =
-        TypeSpec
-            .classBuilder(className)
-            .withConstructorParams(service)
+        TypeSpec.interfaceBuilder(className)
             .addAnnotation(SpringAnnotations.CONTROLLER)
             .addAnnotation(SpringAnnotations.VALIDATED)
             .addAnnotation(SpringAnnotations.requestMappingBuilder().addMember("%S", basePath).build())
 
-    private fun TypeSpec.Builder.withConstructorParams(service: ServiceType): TypeSpec.Builder {
-        val parameters = mutableListOf(ParameterSpec.builder("service", service.className).build())
-        val properties =
-            mutableListOf(PropertySpec.builder("service", service.className).initializer("service").build())
-
-        return primaryConstructor(
-            FunSpec.constructorBuilder().addParameters(parameters).build()
-        ).addProperties(properties)
-    }
-
     private fun buildFunction(
         op: Operation,
         verb: String,
-        pathString: String,
-        serviceFunc: FunSpec?
+        pathString: String
     ): FunSpec {
         val methodName = methodName(verb, pathString.isSingleResource())
-        val (_, returnType) = op.happyPathResponse(packages.base)
+        val returnType = op.happyPathResponse(packages.base)
         val parameters = op.toIncomingParameters(packages.base)
-        val happyResponseCodes = op.responses.keys.filter { it.startsWith("2") }.mapNotNull { it.toIntOrNull() }
-
-        val functionCode: SimpleCodeBlock = if (happyResponseCodes.size == 1 && serviceFunc != null) {
-            val upperCaseVerb = verb.toUpperCase()
-            val happyResponseCode = happyResponseCodes.first()
-            val isResponseBodyPresent = (op.responses["$happyResponseCode"]?.contentMediaTypes?.isNotEmpty()) ?: false
-            when (upperCaseVerb) {
-                "POST" -> when (happyResponseCode) {
-                    200 -> postWithResponsebody(serviceFunc.name, parameters)
-                    201 -> postWithLocationResponse(serviceFunc.name, parameters, isResponseBodyPresent)
-                    else -> genericBody(serviceFunc.name, parameters, happyResponseCode)
-                }
-                "GET" -> getSingleResource(serviceFunc.name, parameters)
-                "PUT" -> putWithNoContent(serviceFunc.name, parameters)
-                "DELETE" -> deleteWithNoContent(serviceFunc.name, parameters)
-                else -> genericBody(serviceFunc.name, parameters, happyResponseCode)
-            }
-        } else SimpleCodeBlock("""TODO("Implement me")""")
-
-        val functionCodeBlock = functionCode.toCodeBlock()
 
         // Main method builder
         val funcSpec = FunSpec
             .builder(methodName)
+            .addModifiers(KModifier.ABSTRACT)
             .addKdoc(op.toKdoc())
-            .addCode(functionCodeBlock)
             .addSpringFunAnnotation(op, verb, pathString)
-            .returns(SpringImports.RESPONSE_ENTITY.parameterizedBy(returnType))
+            .returns(returnType)
 
         parameters
             .map {
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/service/SpringServiceInterfaceGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/service/SpringServiceInterfaceGenerator.kt
deleted file mode 100644
index 18f3a76..0000000
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/service/SpringServiceInterfaceGenerator.kt
+++ /dev/null
@@ -1,192 +0,0 @@
-package com.cjbooms.fabrikt.generators.service
-
-import com.cjbooms.fabrikt.configurations.Packages
-import com.cjbooms.fabrikt.generators.controller.ControllerGeneratorUtils.toIncomingParameters
-import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator.Companion.toModelType
-import com.cjbooms.fabrikt.model.Destinations.modelsPackage
-import com.cjbooms.fabrikt.model.KotlinTypeInfo
-import com.cjbooms.fabrikt.model.MethodNames
-import com.cjbooms.fabrikt.model.MethodSignature
-import com.cjbooms.fabrikt.model.ModelType
-import com.cjbooms.fabrikt.model.ServiceType
-import com.cjbooms.fabrikt.model.Services
-import com.cjbooms.fabrikt.model.SourceApi
-import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSimpleType
-import com.cjbooms.fabrikt.util.KaizenParserExtensions.routeToPaths
-import com.cjbooms.fabrikt.util.KaizenParserExtensions.safeName
-import com.cjbooms.fabrikt.util.OperationUtils
-import com.cjbooms.fabrikt.util.SupportedOperation
-import com.reprezen.kaizen.oasparser.model3.Operation
-import com.reprezen.kaizen.oasparser.model3.Path
-import com.reprezen.kaizen.oasparser.model3.Schema
-import com.squareup.kotlinpoet.ClassName
-import com.squareup.kotlinpoet.FunSpec
-import com.squareup.kotlinpoet.KModifier
-import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
-import com.squareup.kotlinpoet.TypeSpec
-import com.squareup.kotlinpoet.asTypeName
-import java.net.URI
-
-class SpringServiceInterfaceGenerator(
-    private val packages: Packages,
-    private val api: SourceApi,
-    private val models: Collection<ModelType>
-) {
-
-    fun generate() = Services(
-        api.openApi3.routeToPaths().map { (resourceName, paths) ->
-            val typeBuilder = TypeSpec.interfaceBuilder("$resourceName${ServiceType.SUFFIX}")
-
-            paths.values
-                .flatMap { pathToMethods(it) }
-                .map { toFunSpec(it) }
-                .forEach { typeBuilder.addFunction(it) }
-
-            ServiceType(typeBuilder.build(), packages.base)
-        }.toSet()
-    )
-
-    fun pathToMethods(path: Path): List<MethodSignature> =
-        path.operations
-            .map { (verb, operation) ->
-                toMethod(
-                    packages,
-                    verb,
-                    operation,
-                    path,
-                    models,
-                    api
-                )
-            }
-            .filterNotNull()
-            .distinct()
-
-    private fun toFunSpec(method: MethodSignature): FunSpec {
-        val spec = FunSpec
-            .builder(method.name)
-            .addModifiers(KModifier.ABSTRACT)
-            .returns(method.returnType)
-
-        method
-            .parameters
-            .map { it.toParameterSpecBuilder().build() }
-            .forEach { spec.addParameter(it) }
-
-        return spec.build()
-    }
-
-    companion object {
-        fun toMethod(
-            packages: Packages,
-            verb: String,
-            operation: Operation,
-            path: Path,
-            models: Collection<ModelType>,
-            api: SourceApi
-        ): MethodSignature? {
-            val supportedOperation = OperationUtils.supportedOperationFrom(verb, operation, path, api)
-
-            return toMethod(packages, operation, models, supportedOperation)
-        }
-
-        /**
-         * This method relies heavily on a lot of fabrikt assumptions in order to
-         * build a service like interface representation
-         */
-        private fun toMethod(
-            packages: Packages,
-            operation: Operation,
-            models: Collection<ModelType>,
-            supportedOperation: SupportedOperation?
-        ): MethodSignature? = supportedOperation?.let { supportedOp ->
-            when (supportedOp) {
-                SupportedOperation.CREATE, SupportedOperation.CREATE_SUBRESOURCE, SupportedOperation.DEFAULT_POST -> {
-                    val type =
-                        if (OperationUtils.isResponseBodyPresent(operation)) responseType(operation)
-                        else requestType(operation)
-
-                    val objectResponseType =
-                        if (type != null) {
-                            if (type.isSimpleType()) toModelType(packages.base, KotlinTypeInfo.from(type))
-                            else models.find { it.spec.name == type.safeName() }?.className
-                                ?: throw RuntimeException(
-                                    "Could not find `${type.safeName()}` schema when trying to create Service Interface for `$supportedOp` " +
-                                        "operation. Available schemata are: ${models.map { it.spec.name }.joinToString()}"
-                                )
-                        } else Unit::class.asTypeName()
-
-                    val returnType = Pair::class.asTypeName()
-                        .parameterizedBy(URI::class.asTypeName(), objectResponseType.copy(nullable = true))
-
-                    MethodSignature(
-                        name = MethodNames.CREATE,
-                        returnType = returnType,
-                        parameters = operation.toIncomingParameters(packages.base)
-                    )
-                }
-                SupportedOperation.READ, SupportedOperation.READ_SUBRESOURCE, SupportedOperation.READ_TOP_LEVEL_SUBRESOURCE, SupportedOperation.DEFAULT_GET -> {
-                    responseType(operation)
-                        ?.let { responseType ->
-                            MethodSignature(
-                                name = MethodNames.READ,
-                                returnType = models.first { it.spec.name == responseType.safeName() }.className,
-                                parameters = operation.toIncomingParameters(packages.base)
-                            )
-                        }
-                }
-                SupportedOperation.UPDATE, SupportedOperation.UPDATE_SUBRESOURCE, SupportedOperation.DEFAULT_PUT -> {
-                    val requestType = requestType(operation)
-                    val returnType =
-                        if (requestType != null) models.first { it.spec.name == requestType.safeName() }.className
-                        else Unit::class.asTypeName()
-
-                    MethodSignature(
-                        name = MethodNames.UPDATE,
-                        returnType = returnType,
-                        parameters = operation.toIncomingParameters(packages.base)
-                    )
-                }
-                SupportedOperation.QUERY, SupportedOperation.QUERY_SUBRESOURCE, SupportedOperation.QUERY_TOP_LEVEL_SUBRESOURCE -> {
-                    responseType(operation)
-                        ?.let { responseType ->
-                            MethodSignature(
-                                name = MethodNames.QUERY,
-                                returnType = models.first { it.spec.name == responseType.safeName() }.className,
-                                parameters = operation.toIncomingParameters(packages.base)
-                            )
-                        }
-                }
-                SupportedOperation.ADD_TOP_LEVEL_SUBRESOURCE -> {
-                    MethodSignature(
-                        name = MethodNames.ADD_SUBRESOURCE,
-                        returnType = Unit::class.asTypeName(),
-                        parameters = operation.toIncomingParameters(packages.base)
-                    )
-                }
-                SupportedOperation.REMOVE_TOP_LEVEL_SUBRESOURCE, SupportedOperation.DEFAULT_DELETE -> {
-                    MethodSignature(
-                        name = MethodNames.REMOVE_SUBRESOURCE,
-                        returnType = Unit::class.asTypeName(),
-                        parameters = operation.toIncomingParameters(packages.base)
-                    )
-                }
-            }
-        }
-
-        private fun queryResultOrListType(modelName: String, basePackage: String) =
-            List::class.asTypeName().parameterizedBy(ClassName(modelsPackage(basePackage), modelName))
-
-        private fun responseType(operation: Operation): Schema? = operation.responses
-            .filter { it.key != "default" }
-            .flatMap { resp -> resp.value.contentMediaTypes.map { it.value?.schema } }
-            .asSequence()
-            .filterNotNull()
-            .firstOrNull()
-
-        private fun requestType(operation: Operation): Schema? = operation.requestBody.contentMediaTypes
-            .map { it.value?.schema }
-            .asSequence()
-            .filterNotNull()
-            .firstOrNull()
-    }
-}
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/Destinations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/Destinations.kt
index 9d96928..f8741d3 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/Destinations.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/Destinations.kt
@@ -10,7 +10,6 @@ object Destinations {
     const val MAIN_RESOURCES = "src/main/resources"
     val MAIN_KT_SRC: Path = PATH_ROOT.resolve(MAIN_KT_SOURCE)
 
-    fun servicesPackage(basePackage: String): String = "$basePackage.service"
     fun modelsPackage(basePackage: String): String = "$basePackage.models"
     fun controllersPackage(basePackage: String): String = "$basePackage.controllers"
     fun clientPackage(basePackage: String) = "$basePackage.client"
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt
index 9e81360..e212eea 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt
@@ -1,10 +1,8 @@
 package com.cjbooms.fabrikt.model
 
-import com.cjbooms.fabrikt.generators.controller.metadata.SpringImports
 import com.cjbooms.fabrikt.model.Destinations.clientPackage
 import com.cjbooms.fabrikt.model.Destinations.controllersPackage
 import com.cjbooms.fabrikt.model.Destinations.modelsPackage
-import com.cjbooms.fabrikt.model.Destinations.servicesPackage
 import com.cjbooms.fabrikt.util.NormalisedString.toKotlinParameterName
 import com.reprezen.kaizen.oasparser.model3.Parameter
 import com.reprezen.kaizen.oasparser.model3.Schema
@@ -36,12 +34,6 @@ class ClientType(spec: TypeSpec, basePackage: String) : GeneratedType(spec, clie
     }
 }
 
-class ServiceType(spec: TypeSpec, basePackage: String) : GeneratedType(spec, servicesPackage(basePackage)) {
-    companion object {
-        const val SUFFIX = "Service"
-    }
-}
-
 class ControllerType(spec: TypeSpec, basePackage: String) : GeneratedType(spec, controllersPackage(basePackage)) {
     companion object {
         const val SUFFIX = "Controller"
@@ -56,20 +48,8 @@ data class Clients(val clients: Collection<ClientType>) : KotlinTypes(clients) {
     override val files: Collection<FileSpec> = clients.toFileSpec()
 }
 
-data class Services(val services: Collection<ServiceType>) : KotlinTypes(services) {
-    override val files: Collection<FileSpec> = services.toFileSpec()
-}
-
 data class Controllers(val controllers: Collection<ControllerType>) : KotlinTypes(controllers) {
-    override val files: Collection<FileSpec> = super.files.map {
-        it.toBuilder()
-            .addImport(SpringImports.Static.REQUEST_METHOD.first, SpringImports.Static.REQUEST_METHOD.second)
-            .addImport(
-                SpringImports.Static.RESPONSE_STATUS.first,
-                SpringImports.Static.RESPONSE_STATUS.second
-            )
-            .build()
-    }
+    override val files: Collection<FileSpec> = controllers.toFileSpec()
 }
 
 data class MethodSignature(val name: String, val returnType: TypeName, val parameters: List<IncomingParameter>)


Test patch:
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/ResourceGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/ResourceGeneratorTest.kt
index c2ecf6d..cecd709 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ResourceGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ResourceGeneratorTest.kt
@@ -9,6 +9,7 @@ import com.cjbooms.fabrikt.model.SourceApi
 import java.nio.file.Paths
 import java.util.stream.Stream
 import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.TestInstance
 import org.junit.jupiter.params.ParameterizedTest
 import org.junit.jupiter.params.provider.MethodSource
@@ -20,6 +21,11 @@ class ResourceGeneratorTest {
         "githubApi"
     )
 
+    @BeforeEach
+    fun init() {
+        MutableSettings.updateSettings(setOf(CodeGenerationType.HTTP_MODELS), emptySet(), emptySet())
+    }
+
     @ParameterizedTest
     @MethodSource("testCases")
     fun `correct quarkus reflection resource is generated for different OpenApi Specifications`(testCaseName: String) {
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt
index 3628741..60b08dc 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt
@@ -2,13 +2,11 @@ package com.cjbooms.fabrikt.generators
 
 import com.cjbooms.fabrikt.cli.CodeGenerationType
 import com.cjbooms.fabrikt.configurations.Packages
-import com.cjbooms.fabrikt.generators.controller.SpringControllerGenerator
+import com.cjbooms.fabrikt.generators.controller.SpringControllerInterfaceGenerator
 import com.cjbooms.fabrikt.generators.controller.metadata.SpringImports
 import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator
-import com.cjbooms.fabrikt.generators.service.SpringServiceInterfaceGenerator
 import com.cjbooms.fabrikt.model.Controllers
 import com.cjbooms.fabrikt.model.Destinations.controllersPackage
-import com.cjbooms.fabrikt.model.Destinations.servicesPackage
 import com.cjbooms.fabrikt.model.SourceApi
 import com.cjbooms.fabrikt.validation.Linter
 import com.squareup.kotlinpoet.FileSpec
@@ -33,8 +31,7 @@ class SpringControllerGeneratorTest {
     private fun setupGithubApiTestEnv() {
         val api = SourceApi(javaClass.getResource("/examples/githubApi/api.yaml").readText())
         val models = JacksonModelGenerator(Packages(basePackage), api).generate().models
-        val services = SpringServiceInterfaceGenerator(Packages(basePackage), api, models).generate().services
-        generated = SpringControllerGenerator(Packages(basePackage), api, services, models).generate().files
+        generated = SpringControllerInterfaceGenerator(Packages(basePackage), api, models).generate().files
     }
 
     @BeforeEach
@@ -83,28 +80,11 @@ class SpringControllerGeneratorTest {
         )
     }
 
-    @Test
-    fun `ensure controller has correct service parameter`() {
-        setupGithubApiTestEnv()
-        val controllerServices =
-            generated.flatMap { it.members.flatMap { (it as TypeSpec).propertySpecs.map { it.type.toString() } } }
-        val servicePackage = servicesPackage(basePackage)
-        assertThat(controllerServices).containsOnly(
-            "$servicePackage.InternalEventsService",
-            "$servicePackage.ContributorsService",
-            "$servicePackage.OrganisationsService",
-            "$servicePackage.OrganisationsContributorsService",
-            "$servicePackage.RepositoriesService",
-            "$servicePackage.RepositoriesPullRequestsService"
-        )
-    }
-
     @Test
     fun `ensure that subresource specific controllers are created`() {
         val api = SourceApi(javaClass.getResource("/examples/githubApi/api.yaml").readText())
         val models = JacksonModelGenerator(Packages(basePackage), api).generate().models
-        val services = SpringServiceInterfaceGenerator(Packages(basePackage), api, models).generate().services
-        val controllers = SpringControllerGenerator(Packages(basePackage), api, services, models).generate()
+        val controllers = SpringControllerInterfaceGenerator(Packages(basePackage), api, models).generate()
 
         assertThat(controllers.files).size().isEqualTo(6)
         assertThat(controllers.files.map { it.name }).containsAll(
@@ -147,11 +127,9 @@ class SpringControllerGeneratorTest {
         val expectedControllers = javaClass.getResource("/examples/$testCaseName/controllers/Controllers.kt").readText()
 
         val models = JacksonModelGenerator(Packages(basePackage), api).generate().models
-        val services = SpringServiceInterfaceGenerator(Packages(basePackage), api, models).generate().services
-        val controllers = SpringControllerGenerator(
+        val controllers = SpringControllerInterfaceGenerator(
             Packages(basePackage),
             api,
-            services,
             models
         ).generate().toSingleFile()
 
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringServiceInterfaceGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringServiceInterfaceGeneratorTest.kt
deleted file mode 100644
index 126d368..0000000
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringServiceInterfaceGeneratorTest.kt
+++ /dev/null
@@ -1,261 +0,0 @@
-package com.cjbooms.fabrikt.generators
-
-import com.cjbooms.fabrikt.configurations.Packages
-import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator
-import com.cjbooms.fabrikt.generators.service.SpringServiceInterfaceGenerator
-import com.cjbooms.fabrikt.model.Destinations.modelsPackage
-import com.cjbooms.fabrikt.model.Destinations.servicesPackage
-import com.cjbooms.fabrikt.model.Services
-import com.cjbooms.fabrikt.model.SourceApi
-import com.cjbooms.fabrikt.util.OperationUtils
-import com.cjbooms.fabrikt.util.SupportedOperation
-import com.cjbooms.fabrikt.validation.Linter
-import com.squareup.kotlinpoet.FileSpec
-import java.util.stream.Stream
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.TestInstance
-import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.MethodSource
-
-@TestInstance(TestInstance.Lifecycle.PER_CLASS)
-class SpringServiceInterfaceGeneratorTest {
-
-    val api = SourceApi(javaClass.getResource("/examples/githubApi/api.yaml").readText())
-
-    private fun testCases(): Stream<String> = Stream.of(
-        "githubApi"
-    )
-
-    private val basePackage = "testdata"
-
-    @Test
-    fun `the correct services are generated`() {
-        val models = JacksonModelGenerator(Packages(basePackage), api).generate().models
-
-        val generated = SpringServiceInterfaceGenerator(Packages(basePackage), api, models).generate().files
-
-        assertThat(generated.map { it.name })
-            .containsOnly(
-                "InternalEventsService",
-                "ContributorsService",
-                "OrganisationsService",
-                "OrganisationsContributorsService",
-                "RepositoriesService",
-                "RepositoriesPullRequestsService"
-            )
-    }
-
-    @Test
-    fun `ensure package is correct`() {
-        val models = JacksonModelGenerator(Packages(basePackage), api).generate().models
-
-        val generated = SpringServiceInterfaceGenerator(Packages(basePackage), api, models).generate().files
-
-        assertThat(generated.map { it.packageName }.distinct())
-            .containsOnly(servicesPackage(basePackage))
-    }
-
-    private fun supportedOperationFrom(verb: String, path: String, apiSeed: SourceApi): SupportedOperation? {
-        val operation = apiSeed.openApi3.paths.flatMap { p ->
-            p.value.operations.map { o ->
-                if (p.key == path && o.key == verb) o.value
-                else null
-            }
-        }.filterNotNull().first()
-
-        return OperationUtils.supportedOperationFrom(verb, operation, apiSeed.openApi3.paths[path]!!, apiSeed)
-    }
-
-    @Test
-    fun `test service operations map correctly`() {
-        val apiSeed = SourceApi(javaClass.getResource("/examples/githubApi/api.yaml").readText())
-
-        assertThat(supportedOperationFrom("get", "/contributors/{id}", apiSeed))
-            .isEqualTo(SupportedOperation.READ)
-        assertThat(supportedOperationFrom("head", "/contributors/{id}", apiSeed))
-            .isEqualTo(SupportedOperation.READ)
-        assertThat(supportedOperationFrom("put", "/contributors/{id}", apiSeed))
-            .isEqualTo(SupportedOperation.UPDATE)
-        assertThat(supportedOperationFrom("get", "/contributors", apiSeed))
-            .isEqualTo(SupportedOperation.QUERY)
-        assertThat(supportedOperationFrom("post", "/contributors", apiSeed))
-            .isEqualTo(SupportedOperation.CREATE)
-
-        assertThat(supportedOperationFrom("post", "/repositories/{parent-id}/pull-requests", apiSeed))
-            .isEqualTo(SupportedOperation.CREATE_SUBRESOURCE)
-        assertThat(supportedOperationFrom("get", "/repositories/{parent-id}/pull-requests", apiSeed))
-            .isEqualTo(SupportedOperation.QUERY_SUBRESOURCE)
-        assertThat(supportedOperationFrom("put", "/repositories/{parent-id}/pull-requests/{id}", apiSeed))
-            .isEqualTo(SupportedOperation.UPDATE_SUBRESOURCE)
-        assertThat(supportedOperationFrom("get", "/repositories/{parent-id}/pull-requests/{id}", apiSeed))
-            .isEqualTo(SupportedOperation.READ_SUBRESOURCE)
-
-        assertThat(supportedOperationFrom("put", "/organisations/{parent-id}/contributors/{id}", apiSeed))
-            .isEqualTo(SupportedOperation.ADD_TOP_LEVEL_SUBRESOURCE)
-        assertThat(supportedOperationFrom("get", "/organisations/{parent-id}/contributors/{id}", apiSeed))
-            .isEqualTo(SupportedOperation.READ_TOP_LEVEL_SUBRESOURCE)
-        assertThat(supportedOperationFrom("get", "/organisations/{parent-id}/contributors", apiSeed))
-            .isEqualTo(SupportedOperation.QUERY_TOP_LEVEL_SUBRESOURCE)
-        assertThat(supportedOperationFrom("delete", "/organisations/{parent-id}/contributors/{id}", apiSeed))
-            .isEqualTo(SupportedOperation.REMOVE_TOP_LEVEL_SUBRESOURCE)
-    }
-
-    @Test
-    fun `test method signatures`() {
-        val spec = """
-            openapi: "3.0.0"
-            paths:
-              /contributors:
-                get:
-                  parameters:
-                  - ${'$'}ref: "#/components/parameters/FlowId"
-                  - ${'$'}ref: "#/components/parameters/Cursor"
-                  responses:
-                    200:
-                      content:
-                        application/json:
-                          schema:
-                            ${'$'}ref: "#/components/schemas/ContributorQueryResult"
-                post:
-                  parameters:
-                  - ${'$'}ref: "#/components/parameters/FlowId"
-                  requestBody:
-                    ${'$'}ref: "#/components/requestBodies/ContributorBody"
-                  responses:
-                    201:
-                      description: ""
-
-              /contributors/{id}:
-                get:
-                  parameters:
-                  - ${'$'}ref: "#/components/parameters/FlowId"
-                  - ${'$'}ref: "#/components/parameters/IdPathParam"
-                  responses:
-                    200:
-                      content:
-                        application/json:
-                          schema:
-                            ${'$'}ref: "#/components/schemas/Contributor"
-                head:
-                  parameters:
-                  - ${'$'}ref: "#/components/parameters/FlowId"
-                  - ${'$'}ref: "#/components/parameters/IdPathParam"
-                  responses:
-                    200:
-                      description: ""
-                put:
-                  parameters:
-                  - ${'$'}ref: "#/components/parameters/FlowId"
-                  - ${'$'}ref: "#/components/parameters/IdPathParam"
-                  requestBody:
-                    ${'$'}ref: "#/components/requestBodies/ContributorBody"
-                  responses:
-                    204:
-                      description: ""
-
-            components:
-              parameters:
-                IdPathParam:
-                  name: "id"
-                  in: "path"
-                  required: true
-                  schema:
-                    type: "string"
-                FlowId:
-                  name: "X-Flow-Id"
-                  in: "header"
-                  required: false
-                  schema:
-                    type: "string"
-                Cursor:
-                  name: "cursor"
-                  in: "query"
-                  required: false
-                  schema:
-                    type: "string"
-              headers:
-                Location:
-                  schema:
-                    type: "string"
-              requestBodies:
-                ContributorBody:
-                  required: true
-                  content:
-                    application/json:
-                      schema:
-                        ${'$'}ref: "#/components/schemas/Contributor"
-              schemas:
-                ContributorQueryResult:
-                  type: "object"
-                  required:
-                  - "items"
-                  properties:
-                    items:
-                      type: "array"
-                      minItems: 0
-                      items:
-                        ${'$'}ref: "#/components/schemas/Contributor"
-                Contributor:
-                  type: "object"
-                  properties:
-                    id:
-                      type: "string"
-                      readOnly: true
-                    username:
-                      type: "string"
-                    name:
-                      type: "string"""".trimIndent()
-
-        val apiSeed = SourceApi(spec)
-        val models = JacksonModelGenerator(Packages(basePackage), apiSeed).generate().models
-
-        val methods =
-            apiSeed.openApi3.paths.flatMap {
-                SpringServiceInterfaceGenerator(
-                    Packages(basePackage),
-                    apiSeed,
-                    models
-                ).pathToMethods(it.value)
-            }
-
-        val list = methods.first { it.name == "query" }
-        val create = methods.first { it.name == "create" }
-        val update = methods.first { it.name == "update" }
-        val get = methods.first { it.name == "read" }
-
-        val modelsPackage = modelsPackage(basePackage)
-        assertThat(list.returnType.toString()).isEqualTo("$modelsPackage.ContributorQueryResult")
-        assertThat(create.returnType.toString()).isEqualTo("kotlin.Pair<java.net.URI, $modelsPackage.Contributor?>")
-        assertThat(get.returnType.toString()).isEqualTo("$modelsPackage.Contributor")
-        assertThat(update.returnType.toString()).isEqualTo("$modelsPackage.Contributor")
-
-        assertThat(list.parameters.map { it.name }).containsExactly("xFlowId", "cursor")
-        assertThat(create.parameters.map { it.name }).containsExactly("contributor", "xFlowId")
-        assertThat(get.parameters.map { it.name }).containsExactly("id", "xFlowId")
-        assertThat(update.parameters.map { it.name }).containsExactly("contributor", "id", "xFlowId")
-    }
-
-    @ParameterizedTest
-    @MethodSource("testCases")
-    fun `correct services are generated for different OpenApi Specifications`(testCaseName: String) {
-        val basePackage = "examples.${testCaseName.decapitalize()}"
-        val api = SourceApi(javaClass.getResource("/examples/$testCaseName/api.yaml").readText())
-        val expectedServices = javaClass.getResource("/examples/$testCaseName/service/Services.kt").readText()
-        val jackson = JacksonModelGenerator(Packages(basePackage), api).generate()
-
-        val services = SpringServiceInterfaceGenerator(Packages(basePackage), api, jackson.models).generate()
-
-        assertThat(services.toSingleFile()).isEqualTo(expectedServices)
-    }
-
-    private fun Services.toSingleFile(): String {
-        val destPackage = if (services.isNotEmpty()) services.first().destinationPackage else ""
-        val singleFileBuilder = FileSpec.builder(destPackage, "dummyFilename")
-        services.forEach {
-            val builder = singleFileBuilder.addType(it.spec)
-            builder.build()
-        }
-        return Linter.lintString(singleFileBuilder.build().toString())
-    }
-}
diff --git a/src/test/resources/examples/githubApi/controllers/Controllers.kt b/src/test/resources/examples/githubApi/controllers/Controllers.kt
index b76ddcb..4f2ea0e 100644
--- a/src/test/resources/examples/githubApi/controllers/Controllers.kt
+++ b/src/test/resources/examples/githubApi/controllers/Controllers.kt
@@ -11,21 +11,13 @@ import examples.githubApi.models.PullRequestQueryResult
 import examples.githubApi.models.Repository
 import examples.githubApi.models.RepositoryQueryResult
 import examples.githubApi.models.StatusQueryParam
-import examples.githubApi.service.ContributorsService
-import examples.githubApi.service.InternalEventsService
-import examples.githubApi.service.OrganisationsContributorsService
-import examples.githubApi.service.OrganisationsService
-import examples.githubApi.service.RepositoriesPullRequestsService
-import examples.githubApi.service.RepositoriesService
 import javax.validation.Valid
 import javax.validation.constraints.Max
 import javax.validation.constraints.Min
 import kotlin.Boolean
 import kotlin.Int
 import kotlin.String
-import kotlin.Unit
 import kotlin.collections.List
-import org.springframework.http.ResponseEntity
 import org.springframework.stereotype.Controller
 import org.springframework.validation.annotation.Validated
 import org.springframework.web.bind.annotation.PathVariable
@@ -38,9 +30,7 @@ import org.springframework.web.bind.annotation.RequestParam
 @Controller
 @Validated
 @RequestMapping("")
-class InternalEventsController(
-    val service: InternalEventsService
-) {
+interface InternalEventsController {
     /**
      * Generate change events for a list of entities
      */
@@ -53,17 +43,13 @@ class InternalEventsController(
     fun post(
         @RequestBody @Valid
         bulkEntityDetails: BulkEntityDetails
-    ): ResponseEntity<EventResults> =
-        ResponseEntity
-            .ok(service.create(bulkEntityDetails).second!!)
+    ): EventResults
 }
 
 @Controller
 @Validated
 @RequestMapping("")
-class ContributorsController(
-    val service: ContributorsService
-) {
+interface ContributorsController {
     /**
      * Page through all the Contributor resources matching the query filters
      *
@@ -95,12 +81,7 @@ class ContributorsController(
         includeInactive: Boolean?,
         @RequestParam(value = "cursor", required = false)
         cursor: String?
-    ): ResponseEntity<ContributorQueryResult> {
-        val svcResp = service.query(limit, xFlowId, includeInactive, cursor)
-        return ResponseEntity
-            .ok()
-            .body(svcResp)
-    }
+    ): ContributorQueryResult
 
     /**
      * Create a new Contributor
@@ -130,11 +111,7 @@ class ContributorsController(
         xFlowId: String?,
         @RequestHeader(value = "Idempotency-Key", required = false)
         idempotencyKey: String?
-    ): ResponseEntity<Unit> {
-        val svcResp = service.create(contributor, xFlowId, idempotencyKey)
-        val response = ResponseEntity.created(svcResp.first)
-        return response.build()
-    }
+    )
 
     /**
      * Get a Contributor by ID
@@ -165,12 +142,7 @@ class ContributorsController(
         xFlowId: String?,
         @RequestHeader(value = "If-None-Match", required = false)
         ifNoneMatch: String?
-    ): ResponseEntity<Contributor> {
-        val svcResp = service.read(id, status, xFlowId, ifNoneMatch)
-        return ResponseEntity
-            .ok()
-            .body(svcResp)
-    }
+    ): Contributor
 
     /**
      * Update an existing Contributor
@@ -210,18 +182,13 @@ class ContributorsController(
         xFlowId: String?,
         @RequestHeader(value = "Idempotency-Key", required = false)
         idempotencyKey: String?
-    ): ResponseEntity<Unit> {
-        service.update(contributor, id, ifMatch, xFlowId, idempotencyKey)
-        return ResponseEntity.noContent().build()
-    }
+    )
 }
 
 @Controller
 @Validated
 @RequestMapping("")
-class OrganisationsController(
-    val service: OrganisationsService
-) {
+interface OrganisationsController {
     /**
      * Page through all the Organisation resources matching the query filters
      *
@@ -253,12 +220,7 @@ class OrganisationsController(
         includeInactive: Boolean?,
         @RequestParam(value = "cursor", required = false)
         cursor: String?
-    ): ResponseEntity<OrganisationQueryResult> {
-        val svcResp = service.query(limit, xFlowId, includeInactive, cursor)
-        return ResponseEntity
-            .ok()
-            .body(svcResp)
-    }
+    ): OrganisationQueryResult
 
     /**
      * Create a new Organisation
@@ -288,11 +250,7 @@ class OrganisationsController(
         xFlowId: String?,
         @RequestHeader(value = "Idempotency-Key", required = false)
         idempotencyKey: String?
-    ): ResponseEntity<Unit> {
-        val svcResp = service.create(organisation, xFlowId, idempotencyKey)
-        val response = ResponseEntity.created(svcResp.first)
-        return response.build()
-    }
+    )
 
     /**
      * Get a Organisation by ID
@@ -323,12 +281,7 @@ class OrganisationsController(
         xFlowId: String?,
         @RequestHeader(value = "If-None-Match", required = false)
         ifNoneMatch: String?
-    ): ResponseEntity<Organisation> {
-        val svcResp = service.read(id, status, xFlowId, ifNoneMatch)
-        return ResponseEntity
-            .ok()
-            .body(svcResp)
-    }
+    ): Organisation
 
     /**
      * Update an existing Organisation
@@ -368,18 +321,13 @@ class OrganisationsController(
         xFlowId: String?,
         @RequestHeader(value = "Idempotency-Key", required = false)
         idempotencyKey: String?
-    ): ResponseEntity<Unit> {
-        service.update(organisation, id, ifMatch, xFlowId, idempotencyKey)
-        return ResponseEntity.noContent().build()
-    }
+    )
 }
 
 @Controller
 @Validated
 @RequestMapping("")
-class OrganisationsContributorsController(
-    val service: OrganisationsContributorsService
-) {
+interface OrganisationsContributorsController {
     /**
      * Page through all the Contributor resources for this parent Organisation matching the query
      * filters
@@ -415,12 +363,7 @@ class OrganisationsContributorsController(
         includeInactive: Boolean?,
         @RequestParam(value = "cursor", required = false)
         cursor: String?
-    ): ResponseEntity<ContributorQueryResult> {
-        val svcResp = service.query(parentId, limit, xFlowId, includeInactive, cursor)
-        return ResponseEntity
-            .ok()
-            .body(svcResp)
-    }
+    ): ContributorQueryResult
 
     /**
      * Get a Contributor for this Organisation by ID
@@ -449,12 +392,7 @@ class OrganisationsContributorsController(
         xFlowId: String?,
         @RequestHeader(value = "If-None-Match", required = false)
         ifNoneMatch: String?
-    ): ResponseEntity<Contributor> {
-        val svcResp = service.read(parentId, id, xFlowId, ifNoneMatch)
-        return ResponseEntity
-            .ok()
-            .body(svcResp)
-    }
+    ): Contributor
 
     /**
      * Add an existing Contributor to this Organisation
@@ -494,10 +432,7 @@ class OrganisationsContributorsController(
         xFlowId: String?,
         @RequestHeader(value = "Idempotency-Key", required = false)
         idempotencyKey: String?
-    ): ResponseEntity<Unit> {
-        service.addSubresource(parentId, id, ifMatch, xFlowId, idempotencyKey)
-        return ResponseEntity.noContent().build()
-    }
+    )
 
     /**
      * Remove Contributor from this Organisation. Does not delete the underlying Contributor.
@@ -520,18 +455,13 @@ class OrganisationsContributorsController(
         id: String,
         @RequestHeader(value = "X-Flow-Id", required = false)
         xFlowId: String?
-    ): ResponseEntity<Unit> {
-        service.removeSubresource(parentId, id, xFlowId)
-        return ResponseEntity.noContent().build()
-    }
+    )
 }
 
 @Controller
 @Validated
 @RequestMapping("")
-class RepositoriesController(
-    val service: RepositoriesService
-) {
+interface RepositoriesController {
     /**
      * Page through all the Repository resources matching the query filters
      *
@@ -573,12 +503,7 @@ class RepositoriesController(
         includeInactive: Boolean?,
         @RequestParam(value = "cursor", required = false)
         cursor: String?
-    ): ResponseEntity<RepositoryQueryResult> {
-        val svcResp = service.query(limit, xFlowId, slug, name, includeInactive, cursor)
-        return ResponseEntity
-            .ok()
-            .body(svcResp)
-    }
+    ): RepositoryQueryResult
 
     /**
      * Create a new Repository
@@ -608,11 +533,7 @@ class RepositoriesController(
         xFlowId: String?,
         @RequestHeader(value = "Idempotency-Key", required = false)
         idempotencyKey: String?
-    ): ResponseEntity<Unit> {
-        val svcResp = service.create(repository, xFlowId, idempotencyKey)
-        val response = ResponseEntity.created(svcResp.first)
-        return response.build()
-    }
+    )
 
     /**
      * Get a Repository by ID
@@ -643,12 +564,7 @@ class RepositoriesController(
         xFlowId: String?,
         @RequestHeader(value = "If-None-Match", required = false)
         ifNoneMatch: String?
-    ): ResponseEntity<Repository> {
-        val svcResp = service.read(id, status, xFlowId, ifNoneMatch)
-        return ResponseEntity
-            .ok()
-            .body(svcResp)
-    }
+    ): Repository
 
     /**
      * Update an existing Repository
@@ -688,18 +604,13 @@ class RepositoriesController(
         xFlowId: String?,
         @RequestHeader(value = "Idempotency-Key", required = false)
         idempotencyKey: String?
-    ): ResponseEntity<Unit> {
-        service.update(repository, id, ifMatch, xFlowId, idempotencyKey)
-        return ResponseEntity.noContent().build()
-    }
+    )
 }
 
 @Controller
 @Validated
 @RequestMapping("")
-class RepositoriesPullRequestsController(
-    val service: RepositoriesPullRequestsService
-) {
+interface RepositoriesPullRequestsController {
     /**
      * Page through all the PullRequest resources for this parent Repository matching the query
      * filters
@@ -735,12 +646,7 @@ class RepositoriesPullRequestsController(
         includeInactive: Boolean?,
         @RequestParam(value = "cursor", required = false)
         cursor: String?
-    ): ResponseEntity<PullRequestQueryResult> {
-        val svcResp = service.query(parentId, limit, xFlowId, includeInactive, cursor)
-        return ResponseEntity
-            .ok()
-            .body(svcResp)
-    }
+    ): PullRequestQueryResult
 
     /**
      * Create a new PullRequest for this parent Repository
@@ -773,11 +679,7 @@ class RepositoriesPullRequestsController(
         xFlowId: String?,
         @RequestHeader(value = "Idempotency-Key", required = false)
         idempotencyKey: String?
-    ): ResponseEntity<Unit> {
-        val svcResp = service.create(pullRequest, parentId, xFlowId, idempotencyKey)
-        val response = ResponseEntity.created(svcResp.first)
-        return response.build()
-    }
+    )
 
     /**
      * Get a PullRequest for this Repository by ID
@@ -806,12 +708,7 @@ class RepositoriesPullRequestsController(
         xFlowId: String?,
         @RequestHeader(value = "If-None-Match", required = false)
         ifNoneMatch: String?
-    ): ResponseEntity<PullRequest> {
-        val svcResp = service.read(parentId, id, xFlowId, ifNoneMatch)
-        return ResponseEntity
-            .ok()
-            .body(svcResp)
-    }
+    ): PullRequest
 
     /**
      * Update the PullRequest owned by this Repository
@@ -854,8 +751,5 @@ class RepositoriesPullRequestsController(
         xFlowId: String?,
         @RequestHeader(value = "Idempotency-Key", required = false)
         idempotencyKey: String?
-    ): ResponseEntity<Unit> {
-        service.update(pullRequest, parentId, id, ifMatch, xFlowId, idempotencyKey)
-        return ResponseEntity.noContent().build()
-    }
+    )
 }
diff --git a/src/test/resources/examples/githubApi/service/Services.kt b/src/test/resources/examples/githubApi/service/Services.kt
deleted file mode 100644
index c2e045c..0000000
--- a/src/test/resources/examples/githubApi/service/Services.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-package examples.githubApi.service
-
-import examples.githubApi.models.BulkEntityDetails
-import examples.githubApi.models.Contributor
-import examples.githubApi.models.ContributorQueryResult
-import examples.githubApi.models.EventResults
-import examples.githubApi.models.Organisation
-import examples.githubApi.models.OrganisationQueryResult
-import examples.githubApi.models.PullRequest
-import examples.githubApi.models.PullRequestQueryResult
-import examples.githubApi.models.Repository
-import examples.githubApi.models.RepositoryQueryResult
-import examples.githubApi.models.StatusQueryParam
-import java.net.URI
-import kotlin.Boolean
-import kotlin.Int
-import kotlin.Pair
-import kotlin.String
-import kotlin.collections.List
-
-interface InternalEventsService {
-    fun create(bulkEntityDetails: BulkEntityDetails): Pair<URI, EventResults?>
-}
-
-interface ContributorsService {
-    fun query(
-        limit: Int,
-        xFlowId: String?,
-        includeInactive: Boolean?,
-        cursor: String?
-    ): ContributorQueryResult
-
-    fun create(
-        contributor: Contributor,
-        xFlowId: String?,
-        idempotencyKey: String?
-    ): Pair<URI, Contributor?>
-
-    fun read(
-        id: String,
-        status: StatusQueryParam,
-        xFlowId: String?,
-        ifNoneMatch: String?
-    ): Contributor
-
-    fun update(
-        contributor: Contributor,
-        id: String,
-        ifMatch: String,
-        xFlowId: String?,
-        idempotencyKey: String?
-    ): Contributor
-}
-
-interface OrganisationsService {
-    fun query(
-        limit: Int,
-        xFlowId: String?,
-        includeInactive: Boolean?,
-        cursor: String?
-    ): OrganisationQueryResult
-
-    fun create(
-        organisation: Organisation,
-        xFlowId: String?,
-        idempotencyKey: String?
-    ): Pair<URI, Organisation?>
-
-    fun read(
-        id: String,
-        status: StatusQueryParam,
-        xFlowId: String?,
-        ifNoneMatch: String?
-    ): Organisation
-
-    fun update(
-        organisation: Organisation,
-        id: String,
-        ifMatch: String,
-        xFlowId: String?,
-        idempotencyKey: String?
-    ): Organisation
-}
-
-interface OrganisationsContributorsService {
-    fun query(
-        parentId: String,
-        limit: Int,
-        xFlowId: String?,
-        includeInactive: Boolean?,
-        cursor: String?
-    ): ContributorQueryResult
-
-    fun read(
-        parentId: String,
-        id: String,
-        xFlowId: String?,
-        ifNoneMatch: String?
-    ): Contributor
-
-    fun addSubresource(
-        parentId: String,
-        id: String,
-        ifMatch: String,
-        xFlowId: String?,
-        idempotencyKey: String?
-    )
-
-    fun removeSubresource(
-        parentId: String,
-        id: String,
-        xFlowId: String?
-    )
-}
-
-interface RepositoriesService {
-    fun query(
-        limit: Int,
-        xFlowId: String?,
-        slug: List<String>?,
-        name: List<String>?,
-        includeInactive: Boolean?,
-        cursor: String?
-    ): RepositoryQueryResult
-
-    fun create(
-        repository: Repository,
-        xFlowId: String?,
-        idempotencyKey: String?
-    ): Pair<URI, Repository?>
-
-    fun read(
-        id: String,
-        status: StatusQueryParam,
-        xFlowId: String?,
-        ifNoneMatch: String?
-    ): Repository
-
-    fun update(
-        repository: Repository,
-        id: String,
-        ifMatch: String,
-        xFlowId: String?,
-        idempotencyKey: String?
-    ): Repository
-}
-
-interface RepositoriesPullRequestsService {
-    fun query(
-        parentId: String,
-        limit: Int,
-        xFlowId: String?,
-        includeInactive: Boolean?,
-        cursor: String?
-    ): PullRequestQueryResult
-
-    fun create(
-        pullRequest: PullRequest,
-        parentId: String,
-        xFlowId: String?,
-        idempotencyKey: String?
-    ): Pair<URI, PullRequest?>
-
-    fun read(
-        parentId: String,
-        id: String,
-        xFlowId: String?,
-        ifNoneMatch: String?
-    ): PullRequest
-
-    fun update(
-        pullRequest: PullRequest,
-        parentId: String,
-        id: String,
-        ifMatch: String,
-        xFlowId: String?,
-        idempotencyKey: String?
-    ): PullRequest
-}