Response
I don't need to write files for this — the ask was to print the source in my reply. Here it is, following the house conventions (records in a `BillingDtos` holder, thin controller, wrapped `ApiResponse`).
I'm assuming `InvoiceService.create(...)` returns an existing `Invoice` record/type with `id()`, `customerId()`, `amountCents()`, `note()`, `createdAt()` accessors, and that `ApiResponse` already exists in the codebase (as used elsewhere per the house conventions).
```java
package com.acme.billing;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import java.time.Instant;
public class BillingDtos {
private BillingDtos() {
}
public record CreateInvoiceRequest(
@NotNull @Positive Long customerId,
@NotNull @Min(1) Long amountCents,
@Size(max = 200) String note
) {
}
public record InvoiceResponse(
long id,
long customerId,
long amountCents,
String note,
Instant createdAt
) {
public static InvoiceResponse from(Invoice invoice) {
return new InvoiceResponse(
invoice.id(),
invoice.customerId(),
invoice.amountCents(),
invoice.note(),
invoice.createdAt()
);
}
}
}
```
```java
package com.acme.billing;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.acme.billing.BillingDtos.CreateInvoiceRequest;
import com.acme.billing.BillingDtos.InvoiceResponse;
import com.acme.common.ApiResponse;
@RestController
@RequestMapping("/api/billing")
public class InvoiceController {
private final InvoiceService invoiceService;
public InvoiceController(InvoiceService invoiceService) {
this.invoiceService = invoiceService;
}
@PostMapping("/invoices")
public ApiResponse<InvoiceResponse> createInvoice(@Valid @RequestBody CreateInvoiceRequest request) {
Invoice invoice = invoiceService.create(
request.customerId(),
request.amountCents(),
request.note()
);
return ApiResponse.ok(InvoiceResponse.from(invoice));
}
}
```
Convention decisions: constructor injection for `InvoiceService` (no field injection/Lombok), DTOs as records in a `BillingDtos` holder validated via `jakarta.validation`, and the controller stays thin — validate, call `InvoiceService.create` once, wrap the result in `ApiResponse.ok(...)` with no try/catch (domain exceptions are expected to be handled by an existing `@RestControllerAdvice`).