1. Background: "Spring Boot is Great, but My 16GB RAM is Priceless"
After wrapping up the frontend optimization with Astro and React (Episode 6), it was time to design the backend services.
When designing multi-module architectures, the default choice is often Java and Spring Boot (Gradle Multi-Module). With strong typing, mature DI containers, and structured layering, it is undeniably a solid framework.
However, my mini-PC homelab (Ryzen 5500U, 16GB RAM) runs under strict resource constraints. The core database and utility containers already consume a substantial portion of memory:
- PostgreSQL 16 + pgvector (Capped at 4GB for caching vector indexing structures)
- Redis 7.2 Alpine (Capped at 1GB for queuing and caching layers)
- Prometheus + Grafana + Loki (~1GB for the APM and logging stack)
- Gitea + Actions Runner (~200MB for CI/CD pipelines)
In this environment, booting separate Spring Boot instances for the news collector, AI processor, and mail sender would instantly swallow 300MB to 500MB of RAM per instance. Running three JVM processes would eat up 1.5GB of memory before processing a single news article, inviting OS OOM (Out Of Memory) crashes during compute-intensive vector operations.
Furthermore, implementing RAG processing requires Python library bindings (like numpy or sentence-transformers). Choosing Spring Boot meant I would eventually have to launch a separate Python runner anyway, doubling the memory footprint.
Therefore, I chose a resource-optimized stack (ADR-002):
- Node.js (TypeScript) Workspaces: Running high-concurrency async I/O jobs under 50MB of RAM per instance.
- Python RAG Core: Constructing the embedding calculations and pgvector pipelines directly in Python.
- Monorepo Structure: Housing both languages inside a unified repository to keep pipeline configurations clean.
2. The Solution: Node.js & Python Monorepo Layout
My multi-module layout (ADR-002) is organized as follows:
# Monorepo directory structure
AI_News/
├── packages/
│ ├── core/ # [Node.js/TS] Shared database configurations, entities, and schemas
│ ├── collector/ # [Node.js] Asynchronous RSS news collector (batch job)
│ ├── ai-engine/ # [Python] pgvector + OpenRouter RAG summarizer
│ ├── sender/ # [Node.js] Redis Queue based mass email sender
│ └── frontend/ # [Astro/React] Frontend analytics dashboard
├── infra/ # Docker Compose configurations and environment files (.env)
├── docs/ # Architecture Decision Records (ADRs) and blog drafts
├── package.json # Root npm workspaces configuration
└── tsconfig.json # Shared compiler options for TypeScript modules
The core component is packages/core. Since the collector, sender, and frontend dashboard all belong to the Node.js/TypeScript stack, sharing common database entities, DTO schemas, and environment validators inside core keeps code duplication to absolute zero.
3. Implementation: Root Setup & Module Scaffolding
(1) Root Configuration (package.json)
I configured npm workspaces in the root JSON file to manage local dependency paths:
// package.json (root)
{
"name": "@ai-news/root",
"private": true,
"workspaces": [
"packages/core",
"packages/collector",
"packages/sender",
"packages/frontend"
],
"scripts": {
"dev:frontend": "npm run dev -w @ai-news/frontend",
"build:frontend": "npm run build -w @ai-news/frontend",
"start:collector": "npm start -w @ai-news/collector",
"start:sender": "npm start -w @ai-news/sender"
}
}
(2) Shared TypeScript Rules (tsconfig.json)
I established standard compiler options at the root, allowing submodules to extend the configuration:
// tsconfig.json (root)
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true
}
}
(3) The Core Package (packages/core)
Below is the scaffolding setup for the shared TypeScript package:
// packages/core/package.json
{
"name": "@ai-news/core",
"version": "0.1.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc"
}
}
// packages/core/src/types/news.ts
export interface NewsItemDTO {
id?: string;
title: string;
link: string;
source: string;
rawContent: string;
publishedAt: Date;
}
export interface SummaryDTO {
newsId: string;
summaryText: string;
modelUsed: string;
costIncurred: number;
processedAt: Date;
}
4. Troubleshooting & Debugging Logs
🚨 Issue 1: npm Workspaces Hoisting and Missing Local Build Files
After running npm install, trying to boot sub-packages threw compile-time errors: Module not found when referencing @ai-news/core.
-
Cause:
npm workspaces hoist third-party packages to the root node directory. Because the local core package
@ai-news/corewas not built into compiled.jsassets (dist/), referencing submodules crashed during runtime resolve steps. -
Resolution:
I configured package pipeline triggers inside the root script runner, guaranteeing
@ai-news/coreis built prior to booting any dependent workspaces:
// package.json (root)
"scripts": {
"build:core": "npm run build -w @ai-news/core",
"predev:frontend": "npm run build:core",
"prestart:collector": "npm run build:core"
}
🚨 Issue 2: Docker Bridge DNS Resolving Failures (Connection Refused)
Routing API calls from the Node.js collector to the Python AI engine via http://localhost:8000/summarize resulted in crash errors: ECONNREFUSED inside container task runners.
-
Cause:
In containerized spaces,
localhostpoints to the isolated loopback interface of that specific container. To communicate with external containers, processes must resolve network domains using Docker's internal bridge DNS resolver. -
Resolution:
I targeted the Docker Compose service name (
trisoft-ai-engine) as the domain destination, mapping host targets tohttp://trisoft-ai-engine:8000to establish container-to-container connections.
5. Results & Metrics: Spring Boot vs. Node.js/Python
I measured the server memory footprint (RSS) against a simulated Java/Spring Boot multi-module deployment under idle conditions:
| Metric | Spring Boot (JVM, 3 instances) | Node.js (2 instances) + Python (1 instance) | Improvement |
|---|---|---|---|
| Constant RAM Footprint | ~720 MiB | ~124 MiB (Node: 44MB, Python: 80MB) | -82.7% (RAM Saved) |
| Cold Start Duration | ~8.2s | ~0.9s | -89.0% |
| 16GB Host RAM Saved | ~95.5% conserved | ~99.2% conserved (600MB preserved) | +3.7% headroom |
By deploying lightweight Node.js script runtimes for collectors and senders, I freed up 600MB of RAM. This memory buffer was funneled into the pgvector similarity index caches, enabling faster semantic processing pipelines.
6. Next Up
With the Monorepo skeleton verified, Episode 8 will dive into our data harvesting layers: Building a Node.js RSS News Collector Scheduler — Pipeline Steps and DB storage.

Top comments (0)