The legacy ad server had to go.

It served ads when the primary ad system went down. It also handled dashboard previews, external platform integrations, and test requests. The codebase was old — maintenance costs kept climbing.

Removing the legacy meant building something to take over its role. I designed a dedicated fallback server for failure recovery.

Requirements

The conditions were clear.

The fallback server receives no traffic when the primary system runs normally. It activates only when the primary system determines it cannot serve ads. Reducing server costs during idle time was one of the goals.

When a request arrives, the server must handle candidate retrieval, filtering, and response generation in a single pass. Multiple filters each reference independent data sources.

Complex business logic concentrated in a single API endpoint.

Technology Stack

I chose Nest.js. The team’s operational environment centered on Node.js/TypeScript, and Nest.js’s DI container and Module system seemed well-suited for organizing business logic at this scale.

For the HTTP server, I picked Fastify over Express. Throughput matters for an ad server, and Fastify delivers better performance with the same API surface.

TypeORM handled the ORM layer. Ad data and recommendation data lived in separate databases, requiring multi-datasource connections — something TypeORM supports natively.

The design philosophy and core concepts of Nest.js are covered in Nest.js Fundamentals — DI and Module System.

Architecture Decision

Nest.js defaults to vertical module slicing. Each domain gets its own module containing a Controller, Service, and Repository. A user module, a product module, an order module.

This approach did not fit the project.

Domain boundaries were unclear. The server had ad retrieval, filtering, and user parsing, but these were not independent domains. Every request followed the same flow, and every feature contributed sequentially to producing a single response.

I chose a horizontal layered architecture instead. Layers divided by technical responsibility.

  • Presentation: HTTP request/response handling. Controllers, Interceptors, and Filters belong here.
  • Application: Business logic. Orchestrates multiple services and transforms data. Interfaces for infrastructure dependencies are defined in this layer.
  • Domain: Business entities. Pure models with no technical dependencies.
  • Infra: External system integration. Database access, Redis cache, and external API call implementations belong here.

To maintain dependency direction, I applied DIP. The Application layer defines interfaces; the Infra layer provides implementations. Nest.js’s DI container manages these connections through Symbol-based tokens.

Results and Lessons

Validation came when replacing an external component. Swapping an internal implementation for an external library required changes only in the Infra layer. The Application layer’s interfaces remained untouched. Layer separation produced real flexibility.

A colleague suggested adopting vertical module slicing to follow Nest.js conventions. A reasonable suggestion. But this project was not a multi-domain service — it was a single API executing a complex pipeline. Horizontal layering fit that characteristic better.

Service class bloat was a real issue. The filtering service accumulated many filter combinations, making the code lengthy. I addressed this by exposing a Facade to reduce the external interface and splitting internal services into smaller units.

The legacy server had to go. So I built a new one. For a single API with many filters in one pipeline — the framework’s default structure was not the answer.

References