Less ceremony, more value.

Pragmatic notes on backend architecture and software design that prioritizes delivery over dogma.

Simplified monorepo with docker compose for local development, CI and production

Docker Compose is often treated as a “local-only” tool, while CI pipelines reimplement the same logic with custom scripts and duplicated image definitions. That duplication causes drift, surprises, and unnecessary complexity. Using multi-stage Dockerfiles combined with build targets selected via environment variables, Docker Compose can become the single source of truth for: local development production-like local testing CI build CI push No separate compose files. No duplicated CI job definitions per image. ...

December 16, 2025 · 4 min

Rethinking Date Handling in DTOs: Why Strings Make More Sense in NodeJS

Data Transfer Objects (DTOs) are the contracts between client and the server. They need to be simple, serializable, and explicit. Yet over the years NodeJS developers casually drop JavaScript Date objects into DTOs—thinking it’s harmless and convenient. It’s not. Using Date in DTOs introduces subtle bugs, timezone inconsistencies, test fragility, and serialization mismatches. It also violates the basic principle of DTO design: no hidden behavior. Here’s why you should stick to ISO 8601 strings instead—and what can go wrong if you don’t. ...

October 7, 2025 · 6 min

Introducing the 3.1 Maturity Level of REST API Design: JSON Schema-Aware Hypermedia

REST API maturity is often evaluated using the Richardson Maturity Model (RMM), which defines four levels: Level 0 – Single URI, typically using POST for everything Level 1 – Resources identified by URIs Level 2 – Use of HTTP methods to define behavior (GET, POST, PUT, DELETE) Level 3 – Hypermedia controls (HATEOAS) However, traditional REST maturity models often overlook an important aspect: schema awareness. That’s why I would like to propose a new, unofficial but pragmatic step after Level 3: ...

June 13, 2025 · 3 min

Hexagonal Architecture Abuse: When Organization Becomes Obfuscation

Hexagonal architecture (also known as Ports and Adapters) is a powerful way to design software that is independent of frameworks, databases, or UI layers. Its main goal is to isolate the core business logic from the outside world by enforcing clear boundaries. However, in many projects, this pattern is over-engineered to the point where the supposed simplicity turns into a maze of indirection, deeply nested folders, and needless abstractions. The Problem: Overstructuring a Simple Concept Many developers (including me) start with the intent of using hexagonal architecture but quickly spiral into the following: ...

June 11, 2025 · 5 min

Misusing Command and Query Buses in Modular Monoliths: A NestJS Perspective

In recent years, architectural patterns like Command Bus and Query Bus have gained popularity, especially in TypeScript backends using NestJS and its @nestjs/cqrs package. While these patterns offer separation of concerns, they are often overused in places where a simple, clean application service or facade would be better. The Problem: Overengineering in a Modular Monolith Consider this common anti-pattern in NestJS: // update-profile.command.ts export class UpdateProfileCommand { constructor(public readonly userId: string, public readonly nickname: string) {} } // update-profile.handler.ts @CommandHandler(UpdateProfileCommand) export class UpdateProfileHandler implements ICommandHandler { constructor(private readonly userRepository: UserRepository) {} async execute(command: UpdateProfileCommand) { const user = await this.userRepository.findById(command.userId); user.updateNickname(command.nickname); await this.userRepository.save(user); } } // get-profile.query.ts export class GetProfileQuery { constructor(public readonly userId: string) {} } // get-profile.handler.ts @QueryHandler(GetProfileQuery) export class GetProfileHandler implements IQueryHandler { constructor(private readonly userRepository: UserRepository) {} async execute(query: GetProfileQuery) { const user = await this.userRepository.findById(query.userId); return { userId: user.id, nickname: user.nickname }; } } // user-profile.controller.ts await this.commandBus.execute(new UpdateProfileCommand(userId, nickname)); await this.queryBus.execute(new GetProfileQuery(userId)); That’s four classes and four files just to write 2 methods. Where is the value in that? Where is the gain? ...

June 9, 2025 · 7 min