Philosophy

The design principles behind JustScale

The Unified Execution Model

Every entry point is equal.

When an HTTP request arrives, when a CLI command runs, when an internal event fires - JustScale treats them identically. Each invocation creates a fresh execution context with:

  • A new dependency injection scope
  • Full middleware chain execution
  • Guard validation
  • Observability hooks (logging, tracing, metrics)

This isn't just about code reuse. It's about consistency. Your authentication middleware works the same whether protecting an HTTP endpoint or a CLI command. Your logging captures the same context. Your error handling behaves identically.

profile-controller.tsTypeScript
import { createController, createMiddleware } from '@justscale/core';
import { Get } from '@justscale/http';
import { Cli } from '@justscale/cli';
import { AuthService } from './auth-service';
import { UserService } from './user-service';

// This middleware works with ANY transport
const authenticate = createMiddleware({
  inject: { auth: AuthService },
  handler: ({ auth }) => async (ctx: { headers?: { authorization?: string }; args?: { token?: string } }) => {
    const token = ctx.headers?.authorization || ctx.args?.token;
    const user = await auth.verify(token);
    return { user };
  },
});

// Use it on HTTP routes and CLI commands
export const ProfileController = createController('/profile', {
  inject: { users: UserService },
  routes: ({ users }) => ({
    // HTTP endpoint
    http: Get('/').use(authenticate).handle(({ user, res }) => {
      res.json(users.getProfile(user.id));
    }),

    // CLI command - same middleware!
    cli: Cli('show').use(authenticate).handle(({ user, io }) => {
      io.json(users.getProfile(user.id));
    }),
  }),
});

Core Principles

1. Type Safety First

Every aspect of JustScale is designed for compile-time type checking. Missing dependencies, incorrect context properties, and type mismatches are caught before your code runs.

users-controller.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { UserService } from './user-service';

// TypeScript catches this at compile time
export const UsersController = createController('/users', {
  inject: { users: UserService }, // Error if UserService not registered
  routes: ({ users }) => ({
    list: Get('/').handle(({ res }) => {
      res.json({ users: users.findAll() });
    }),
  }),
});

2. Explicit Over Magical

No decorators, no reflection, no hidden behavior. JustScale uses plain functions and objects. What you see is what you get.

Files
user-service.tsTypeScript
import { createService } from '@justscale/core';
import { Database } from './database';

// No decorators - just functions
export const UserService = createService({
  inject: { db: Database },
  factory: ({ db }) => ({
    findAll: () => db.query('SELECT * FROM users'),
  }),
});

3. Composable Architecture

Build applications from small, focused pieces. Services, middleware, guards, and features compose together cleanly.

users-controller.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { UserService } from './user-service';
import { authenticate } from './middleware/authenticate';
import { parseQuery } from './middleware/parse-query';
import { isOwner } from './guards/is-owner';

// Middleware composes - each adds to context
export const UsersController = createController('/users', {
  inject: { users: UserService },
  routes: ({ users }) => ({
    getUser: Get('/:id')
      .use(authenticate)  // Adds { user }
      .use(parseQuery)    // Adds { query }
      .guard(isOwner)     // Checks user.id === params.id
      .handle(({ user, query, params, res }) => {
        // Full type safety on composed context
        res.json(users.findById(params.id));
      }),
  }),
});

4. Transport Agnostic

The transport is just the trigger. HTTP, CLI, events, gRPC - they're all entry points into the same application. Write your business logic once, expose it however you need.

users-controller.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { Cli } from '@justscale/cli';
import { UserService } from './user-service';

export const UsersController = createController('/users', {
  inject: { users: UserService },
  routes: ({ users }) => ({
    // HTTP endpoint
    list: Get('/').handle(({ res }) => {
      res.json(users.findAll());
    }),

    // CLI command - same service, same logic
    listCli: Cli('list').handle(({ io }) => {
      io.table(users.findAll());
    }),
  }),
});

5. Minimal Runtime Overhead

JustScale does its heavy lifting at compile time. The runtime is lean - just function calls and object lookups.

  • No reflection or metadata parsing
  • No decorator processing
  • No runtime type checking (TypeScript handles it)
  • Dependency injection is just object property access

Write for One, Scale to Many

The best code doesn't know it's distributed.

When you write a goroutine in Go, you write straightforward, synchronous-looking code: read a value, process it, send a result. You don't think about thread pools, context switching, or CPU scheduling. The Go runtime handles those complexities invisibly. Your code stays simple, readable, and maintainable while the runtime makes it concurrent.

JustScale brings this same philosophy to distributed systems. You write code as if your application runs on a single instance—acquire a lock, update an entity, store some state—and the framework transparently handles clustering, distributed coordination, and horizontal scaling. Need to ensure only one instance processes a task? Just use using lock = await lockService.acquire(entity). The framework decides whether that's a local mutex or a distributed lock based on your deployment. Your code doesn't change. It just scales.

This is the power of abstraction that scales with you: start local, scale distributed, without rewriting. Write simple imperative code. The framework translates it into the distributed operations your infrastructure requires.

The Pattern: Simplicity at the Surface

This philosophy appears throughout great programming tools:

  • Go's goroutines — Write synchronous-looking code, the runtime makes it concurrent. No manual thread management.
  • React's declarative UI — Describe what the UI should look like, React figures out the minimal DOM mutations. No manual createElement calls.
  • SQL's queries — Express what data you want, the engine chooses indexes and execution plans. No manual data structure traversal.
  • Kubernetes manifests — Declare desired state, the orchestrator handles node assignment, health checks, and restarts. No manual container management.

JustScale follows this tradition: you write simple, instance-local code, and the framework handles distributed concerns automatically. No manual shard routing, no cluster-aware conditionals, no deployment-specific logic. Just business logic that adapts to its environment.

The Execution Flow

Regardless of how a request enters your application, it follows the same flow:

text
Trigger (HTTP/CLI/Event/etc.)

Route Matching

Create Execution Context

Resolve Dependencies (scoped)

Run Middleware Chain

Evaluate Guards

Execute Handler

Return Response

This consistency means you can reason about your application the same way regardless of how it's being accessed. Testing becomes simpler. Debugging becomes predictable. Refactoring becomes safer.

Design Decisions

Why Controllers as Entry Points?

Traditional frameworks treat HTTP handlers, CLI commands, and event handlers as separate concerns. JustScale unifies them because they share the same needs: dependency injection, middleware, guards, and observability.

When you think of controllers as "entry points" rather than "HTTP handlers," the architecture naturally supports multiple transports without code duplication.

Why No Decorators?

Decorators require runtime reflection and emit metadata. They also have limited TypeScript support for type inference. JustScale achieves better type safety with plain functions.

Why Abstract Classes for Repositories?

Abstract classes (vs interfaces) provide runtime tokens for dependency injection and support instanceof checks. They're the best balance of type safety and runtime utility.

Why Separate Transports?

Each transport (HTTP, CLI, etc.) is a separate package. This keeps the core small and lets you only install what you need. It also makes it easy to add new transports without touching core code.