intermediate~2h

CRUD & Exposing Data

Kafka got the data into Postgres. Now the consumer needs its own REST surface so the data is actually usable by clients — closing the loop on the architecture from Module 04.

Learning objectives

  • Beginner: Explain why CRUD read endpoints query a local database rather than reading directly from Kafka.
  • Intermediate: Implement REST CRUD endpoints for data that originated as a consumed Kafka event.
  • Advanced: Design the consumer's REST surface so it stays consistent with the producer's event schema as both evolve.
@RestController @RequestMapping("/v1/books") public class BookController { private final BookRepository bookRepository; @GetMapping("/{bookId}") public ResponseEntity<BookEntity> getBook(@PathVariable Integer bookId) { return bookRepository.findById(bookId) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } @DeleteMapping("/{bookId}") public ResponseEntity<Void> deleteBook(@PathVariable Integer bookId) { bookRepository.deleteById(bookId); return ResponseEntity.noContent().build(); } }

💻 Code example

@RestController @RequestMapping("/v1/books") public class BookController { private final BookRepository bookRepository; @GetMapping("/{bookId}") public ResponseEntity<BookEntity> getBook(@PathVariable Integer bookId) { return bookRepository.findById(bookId) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } @DeleteMapping("/{bookId}") public ResponseEntity<Void> deleteBook(@PathVariable Integer bookId) { bookRepository.deleteById(bookId); return ResponseEntity.noContent().build(); } }
@GetMapping("/v1/libraryevent/{libraryEventId}") public ResponseEntity<LibraryEventEntity> getLibraryEvent(@PathVariable Integer libraryEventId) { return libraryEventRepository.findById(libraryEventId) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); }

◆ Under the hood — why this endpoint lives on the consumer, not the producer

This directly follows from Module 04's architecture: the producer has no database, so it structurally cannot serve reads of persisted state — only the consumer, which owns the data, can. A client wanting "the current state of library event #42" must call the consumer service, even though it originally submitted the change via the producer's POST endpoint. This is the standard shape of CQRS-flavored event-driven systems: writes and reads go through different services connected only by the event stream.

💻 Code example

@GetMapping("/v1/libraryevent/{libraryEventId}") public ResponseEntity<LibraryEventEntity> getLibraryEvent(@PathVariable Integer libraryEventId) { return libraryEventRepository.findById(libraryEventId) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); }

Identical setup to Module 08 — add springdoc-openapi-starter-webmvc-ui, and the consumer's CRUD + GET endpoints get the same auto-generated interactive docs.

✓ Quick recap

Why can't the producer serve a GET for a library event's current state? It has no database — only the consumer, which persists the data, can serve reads of it.

Want a visual for this concept?

Generate a diagram tailored to “CRUD & Exposing Data” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Producer Reliability & Errors← Back to all Kafka & Microservices chapters