Skip to main content

Modular Monolith Backend

I chose to structure the Borrowd backend as a Modular Monolith. It compiles and deploys as a single application unit, but it is strictly partitioned into independent Gradle subprojects with clean domain boundaries.

As a solo developer, deploying a single artifact with one Git push and running unit tests in seconds gives me maximum agility. Splitting a system into microservices too early adds network latency, distributed debugging headaches, and deployment complexity that a single engineer shouldn't have to manage.

Compile-Time Boundary Enforcement​

Gradle project dependencies strictly enforce module boundaries (:server:items, :server:users, :server:photos). Feature modules cannot access each other's private classes, keeping domain logic clean. Cross-module orchestration lives in application.usecase, insulating business workflows.

Extraction-Ready​

Because I enforce domain boundaries at the Gradle build level today, any feature module can be extracted into an independent microservice in the future with minimal code changes.

Core services remain identical whether running in-process or in a remote container. Callers interact exclusively with module facade interfaces.

// In-process facade interface
interface ItemsFacade {
suspend fun get(id: String): ItemDto
}

When extracting a module, I can simply replace the in-process facade implementation with an HTTP or gRPC client, while the consuming use-cases remain entirely unaware of the transport shift.

Reactive Persistence Engine​

Beneath the domain logic, I built the persistence layer using Komapper and R2DBC.

Traditional ORMs like Hibernate pull entire object graphs into memory and block operating system threads during I/O. R2DBC, on the other hand, uses reactive streams to communicate with PostgreSQL. While waiting for database queries to complete, worker threads are freed up to handle other incoming requests.

Code-First SQL DSL

Komapper provides a clean SQL DSL directly in Kotlin code. There are no XML mappings or hidden ORM cascades. The queries are completely type-safe and verifiable at compile time.

I also wrote custom R2DBC codecs (PointType and PostgisCodecRegistrar) to convert PostGIS spatial geometry types into Kotlin objects asynchronously without blocking, forming the foundation of Borrowd's fast location-based search capabilities.