Breaking Up With

My Default Stack

Torres del Paine, Patagonia

Backstage

Storytelling Techniques

Storytelling Techniques

Storytelling Techniques

  • Where are you?
    • Simple. Make the listener imagine what it looks like
  • Actions: What are you doing in that specific point moment
    • Walking, Biking, etc. It brings forward moment, it brings listener to the moment. Listeners they know you are not wasting their time
  • What are your thoughts?
    • "I thought, that would be so cool"; Raw unfiltered, juiicy
  • What are you feeling?
    • Show emotion with body posture

Purpose

Why am I telling this story?

 


What do I want people to know / have learnt?

Patrick Favre

Lead Developer, Senacor
15+ years experience in professional software development. 10+ Projects. 4+ industries
Java Veteran
Starting out with JBoss SEAM (2009), 5y Android, Spring 3/4/Boot 1–3, Java 5-17
Current Focus
Architecture (tech/sociotech), Cloud/Backend. Industry focus FinTech/Banking currently Supervisory
 github.com/patrickfav

i love the java ecosystem

... then I was put on a node project...

..., and now my reality is serverless

Challenges

- Startup of JVM is super slow

- JVM uses lots of RAM, especially threaded webserver under load

Reality

- Customer is incredibly ops cost sensitive

- Focus on on demand resources like Lambdas

Typical Startup Time
20-200ms
Fast enough for syncronous cold start requests
Runtime Comparison (ms)
Runtime behaviour

Node startup times

Typical RAM USAGE
30-300MB
Runtime Comparison: RAM
Runtime Behaviour

Node RAM usage

Traditional Threaded
One Thread per Request

Blocking I/O. Each connection consumes significant memory and waits for database/disk responses.

 
 
 
Node.js Event Loop
Single-Threaded I/O

Non-blocking. Offloads I/O tasks and continues processing other requests in the event loop.

Request Model

Non Blocking vs. Threaded

Async Flow, Serial Feel
Node.js leverages JavaScript's Asynchronous nature, but modern syntax allows us to write it like traditional blocking code.
  •  No complex thread management
  •  Readable business logic

However, requires understanding of micro and macro task queue which might run into starvation bugs if used incorrectly.

Forces Observable APIs
In WebFlux, reactive types (Mono/Flux) are infectious. They must propagate through your entire codebase.
  •  Controllers, Services, Repos... all reactive
  •  Complex operator nesting (flatMap, zip)
  •  Harder to debug and stack trace
Developer Experience

Async/Await vs. Java Reactive Models

vs.

Side-by-Side Comparison

Syntactic Sugar vs. Flowable Complexity

async function saveOrder(order: Order) {
  const user = await db.users.find(order.uid);
  const result = await db.orders.insert(order);
  await email.send(user.email, "Success!");
  return result;
}

// Mock DB
const db = {
  users: {
    find: async (id: string): Promise<User | null> => {
      return { id, email: "user@example.com" };
    },
  },
  ...
};

//Service
const emailService = {
  send: async (email: string, message: string): 
  Promise<void> => {
    console.log(`Email sent to ${email}: ${message}`);
  },
};
public Mono<Order> saveOrder(Order order) {
  return userRepository.findById(order.getUid())
    .flatMap(user -> orderRepository.save(order)
      .flatMap(result -> emailService.send(user.getEmail())
        .thenReturn(result)));
}

// Repository
public interface UserRepository {
    Mono<User> findById(String id);
}

public interface OrderRepository {
    Mono<Order> save(Order order);
}

// Service
public class EmailService {
    public Mono<Void> send(String email, String message) {
        return Mono.fromRunnable(() ->
            System.out.println("Email sent to " + email)
        );
    }
}
Node.js
Spring WebFlux
 

For reference: Coroutines

suspend fun saveOrderParallel(order: Order): Order = coroutineScope {
    val userDeferred = async { userRepository.findById(order.uid) }
    val saveDeferred = async { orderRepository.save(order) }

    val user = userDeferred.await()
        ?: throw IllegalStateException("User not found")

    val savedOrder = saveDeferred.await()

    emailService.send(user.email, "Order success!")

    savedOrder
}
Language

Typescript: From the Java Developers Perspective

Configuration
Strict tsconfig
"Strict" mode is the baseline for usability. It turns the language into a reliable tool rather than a suggestion.
Type System
Sophisticated Type System
Union Types, complex inferred types and conditional types and more allow high flexibility. Structural types make passing data easy.
Easy integration
TS runs natively  
Node, Bun and Deno all allow running TS without transpiration step (with limitations)
{
  "strict": true,
  "noImplicitAny": true,
  "exactOptionalPropertyTypes": true
}
Limitation
Limited by JS
TS community does not like features that are "TS" only and can't just be erased to create JS. Popular example: Enums and namespaces.
Limitation
No Runtime Types
Types, Interfaces, etc. are lost in runtime and incorrect data will not be caught.
Limitation
Nitpicks
No Named Parameters and harder to write elegant immutable code due to not "everything beeing an expression" like in Kotlin; null vs undefined
Language

Typescript allows for sophisticated infered types

TS has no type information during runtime, therefore schema validators, like Zod are often used.


TS allows to infer other, related, types from e.g. a schema.

import { z } from "zod";

export const CustomerSchema = z.object({
  id: z.string().uuid(),
  firstName: z.string().min(1),
  email: z.string().email().optional(),
  loyaltyPoints: z.number().int().nonnegative(),
});

/**
 * Infer TypeScript type from schema
 */
export type Customer = z.infer<typeof CustomerSchema>;

/*
 type Customer = {
    id: string;
    firstName: string;
    email?: string | undefined;
    loyaltyPoints: number;
 }
*/
Language

Typescript has expressive type-level language

The 20+ built-in utility types help to minimize type duplication by creating adapted infered types.

// 1. infer return type with ReturnType<>
function createCustomer() {
  return {
    name: "Alice",
    age: 30,
  };
}

type Customer = ReturnType<typeof createCustomer>; // { name: string; age: number;}

// 2. Extracts parameter types of a function as a tuple
function updateCustomer(id: string, age: number) {
  return true;
}

type UpdateParams = Parameters<typeof updateCustomer>; // [id: string, age: number]

//3. Remove properties from a type
type User = {
  id: string;
  email: string;
  password: string;
};

type PublicCustomer = Omit<User, "password">; // { id: string, email: string;}
ecosystem

Nodes Ecosystem is similarly rich

Build & Package
Maven / Gradle
The classic Java standard for dependency management and lifecycle.
Vite + pnpm
Lightning fast HMR and efficient, symlink-based package resolution.
Testing Frameworks
JUnit / Mockito
The gold standard for TDD and behavioral verification in JVM.
Vitest / Jest
Modern, fast runners with built-in mocking and native ESM support.
runtimes
Oracle, OpenJDK, GraalVM
Wide array different JDKs, JVMs and a native runtime via GraalVM available.
Node, Bun, Deno
Node alternatives often promise better performance but are mostly not "fully node compatible".
Packaging & deployment
JAR/WAR
Explicit compiled runtime package that might be self-contained or needs dependency resolution. 
npm packages
Mainly zipped source code. Either with dependency graph or self-contained with node_moduels or packaged via esbuild/WebPack.
DB & ORM
JPA, JDBC
Wide array for mature ORMs and Database Drivers
TypeORM
Most popular ORM in TS ecosystem with similar DevEx to JPA.
linting & Formatting
Checkstyle, PMD 
Java has a wide array of formatting standards. Complex configuration of formatter like Checkstyle reflect that
ESLint + Prettier
Highly Plugin based, can get slow with complex code bases.
frameworks

Node Web Application Frameworks are mostly "minimalistic"

Philosophy
Minimalism & Speed

Most Node frameworks prioritize a small footprint and raw performance. They provide the bare essentials, letting developers bolt on only what they need.

Express
Fastify
Hono
The Trade-off
Architectural Ownership

Being unopinionated means the framework won't tell you how to structure your folders, handle DI, or manage state.

Requirement
Requires experienced developers to define and enforce a consistent architecture from day one.
frameworks

Introduce "Spring" for Node: NestJS

The Bridge
Spring Boot for TypeScript

NestJS is heavily inspired by Angular and Spring. It brings enterprise-grade structure to Node.js, solving the "architecture-less" problem of Express.

1:1 Mental Models
Spring Bean
Nest Provider
@Component
@Injectable()
Auto-wiring
Constructor DI
Paradigms and features
Modular Architecture
Explicit module definitions that mirror Spring's package-based organization.
Decorators
Uses TS Decorators just like Java Annotations for Routing, DI, and Guards.
Built-in Middlewares
Interceptors, Pipes, and Exception Filters work like Spring Interceptors/AOP.
Testing Framework & CLI
Similar to Spring Boot Test helps with integration tests and CLI to bootstrap projects.
@Controller('users')
export class UserController {
  constructor(private userService: UserService) {}
  @Get()
  findAll() { return this.userService.findAll(); }
}
frameworks

NestJS Example

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.userService.findById(id);
  }

  @Post()
  create(@Body() dto: CreateUserDto) {
    return this.userService.create(dto);
  }
}

Controller

@Injectable()
export class UserService {
  constructor(private readonly repo: UserRepository) {}

  async findById(id: string) {
    const user = await this.repo.findById(id);

    if (!user) {
      throw new NotFoundException(`User ${id} not found`);
    }

    return user;
  }

  create(dto: { name: string; email: string }) {
    return this.repo.save(dto);
  }
}

Service

import { Module } from '@nestjs/common';

@Module({
  controllers: [UserController],
  providers: [UserService, UserRepository],
})
export class UsersModule {}

Module

import { IsEmail, IsString } from 'class-validator';

export class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;
}

Model

security

The ugly: NPMs constant threat of supply chain attack

The Problem
Dependency Bloat

Modern JS projects often inherit 1000+ transitive dependencies. Every single one is a potential vector for malicious code injection.

Primary Attack Vectors
Social Engineering
Gaining maintainer trust to inject malware directly into popular repos.
Stolen Credentials
Compromising npm accounts via phishing or data breaches.
Postinstall Scripts
Executing hidden shell scripts during the installation process.
Typosquatting
Registering similar names (e.g. "lo-dash") to catch developer errors.
Mitigation Techniques
Release Age Filter
Using pnpm to enforce a minimum package age to avoid "day zero" exploits.
Script Whitelisting
Disabling arbitrary execution by only allowing trusted postinstall hooks.
Advanced Auth
Leveraging GitHub's improved 2FA and WebAuthn for secure account access.
Trust Policies
Implementing pnpm trustPolicy and ignoring exotic/unverified dependencies.
ecosystem

Summary & Takeaway

Performance & Efficiency
Node starts reasonably fast and maintains an efficient RAM footprint compared to the JVM.
Non-Blocking I/O
At its core, Node is a non-blocking webserver, perfectly suited for high-concurrency workloads.
The Language
TypeScript is fairly easy to learn for Java developers, offering the type safety and patterns you would expect.
Mature Ecosystem
The ecosystem has matured significantly, providing robust library support for enterprise requirements. Still fragmented and a bit chaotic.
Opinionated Options
Frameworks like NestJS provide "opinionated" options that feel familiar and bridge the architectural gap.
The Security Reality
Critical issues with Supply Chain Security remain the primary hurdle and require active management.
 
Questions?
:)