You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
# Same App, 4x the Code: Anatomy of a Full-Stack Polyglot Tax
11
+
# Same App, 5x the Code: Anatomy of a Full-Stack Polyglot Tax
12
12
13
13
We built the exact same app twice. Once in Jac. Once with FastAPI, SQLAlchemy, LangChain, React, TypeScript, Vite, and Bun — the state-of-the-art (SOTA) stack that a senior engineer would reach for today. Same features, same UI, same AI-powered categorization, same persistence. Both QA-verified to behave identically.
14
14
15
-
The Jac version is **2 files, 82 lines**. The SOTA version is **16 files, 311 lines**.
15
+
The Jac version is **1 file, 46 lines** of application code. The SOTA version is **11 files, 233 lines**.
16
16
17
17
These are good tools. FastAPI is arguably the best Python API framework. SQLAlchemy is the most mature Python ORM. LangChain is the dominant LLM orchestration library. React and TypeScript need no introduction. We picked the best of each category on purpose — this isn't a comparison against straw men.
18
18
19
-
And yet, building with the best-in-class version of every tool still produces nearly 4x more code across 8x more files than expressing the same idea in a language designed to handle the full stack. We're calling this the **polyglot tax** — the code you write not to solve your problem, but to make your tools talk to each other across language, runtime, and type-system boundaries. This article is a line-by-line anatomy of where that tax shows up and what it costs.
19
+
And yet, building with the best-in-class version of every tool still produces ~5x more application code across 11x more files than expressing the same idea in a language designed to handle the full stack. We're calling this the **polyglot tax** — the code you write not to solve your problem, but to make your tools talk to each other across language, runtime, and type-system boundaries. This article is a line-by-line anatomy of where that tax shows up and what it costs.
@@ -363,7 +347,7 @@ That last part — `TodoResponse(id=todo.id, title=todo.title, ...)` — is part
363
347
364
348
**Static file serving** (lines 64-69) — The backend serving the frontend's compiled assets. This exists because the frontend is a separate application. In Jac, `cl def` compiles into the server's asset pipeline — there's nothing to serve separately.
{[<p key={jid(t)}>{t.title} ({t.category})</p> for t in todos]}
409
385
</div>;
410
386
}
411
387
```
@@ -636,183 +612,19 @@ Seven dependencies for a todo app frontend. Notice `@types/react` and `@types/re
636
612
637
613
---
638
614
639
-
## Section 5: Project Configuration — One Config vs Five
640
-
641
-
Every project needs configuration. Dependencies, build settings, compiler options, runtime behavior. The question is how many separate config systems you're managing.
642
-
643
-
### Jac — 34 lines, 1 file
644
-
645
-
```toml
646
-
[project]
647
-
name = "mini-todo"
648
-
version = "1.0.0"
649
-
description = "Minimal full-stack AI todo app"
650
-
entry-point = "main.jac"
651
-
652
-
[dependencies.npm]
653
-
react = "^18.2.0"
654
-
react-dom = "^18.2.0"
655
-
656
-
[dependencies.npm.dev]
657
-
vite = "^6.4.1"
658
-
"@vitejs/plugin-react" = "^4.2.1"
659
-
typescript = "^5.3.3"
660
-
"@types/react" = "^18.2.0"
661
-
"@types/react-dom" = "^18.2.0"
662
-
663
-
[dev-dependencies]
664
-
watchdog = ">=3.0.0"
665
-
666
-
[serve]
667
-
base_route_app = "app"
668
-
669
-
[plugins.scale]
670
-
671
-
[plugins.client]
672
-
673
-
[plugins.byllm.model]
674
-
default_model = "claude-sonnet-4-20250514"
675
-
```
676
-
677
-
One file declares everything: Python dependencies, npm dependencies, dev dependencies, server config, plugin config, and LLM model selection. `jac start` reads this and does the rest — installs packages across both ecosystems, builds the frontend, starts the server.
678
-
679
-
The key observation: both Python and JavaScript dependencies live in the same file. There's no cognitive split between "backend config" and "frontend config" because there's no backend/frontend split.
680
-
681
-
### SOTA — 78 lines across 5 files
682
-
683
-
The same concerns are spread across five files in two different ecosystems.
684
-
685
-
**`pyproject.toml`** — Python dependencies:
686
-
687
-
```toml
688
-
[project]
689
-
name = "mini-todo-sota"
690
-
version = "1.0.0"
691
-
description = "Minimal full-stack AI todo app (SOTA stack)"
692
-
requires-python = ">=3.12"
693
-
dependencies = [
694
-
"fastapi>=0.121.0",
695
-
"uvicorn>=0.38.0",
696
-
"sqlalchemy>=2.0.49",
697
-
"aiosqlite>=0.22.0",
698
-
"langchain>=1.2.0",
699
-
"langchain-anthropic>=1.4.0",
700
-
]
701
-
```
702
-
703
-
Six runtime dependencies for the backend alone: a web framework, a server, an ORM, a database driver, and two LangChain packages. Each of these pulls in its own transitive dependency tree. Jac's backend has zero Python dependencies beyond the Jac runtime itself — the framework, ORM, and LLM integration are language features, not library choices.
704
-
705
-
**`frontend/package.json`** — JavaScript dependencies and build scripts:
706
-
707
-
```json
708
-
{
709
-
"name": "frontend",
710
-
"private": true,
711
-
"version": "0.0.0",
712
-
"type": "module",
713
-
"scripts": {
714
-
"dev": "vite",
715
-
"build": "tsc -b && vite build",
716
-
"preview": "vite preview"
717
-
},
718
-
"dependencies": {
719
-
"react": "^19.2.4",
720
-
"react-dom": "^19.2.4"
721
-
},
722
-
"devDependencies": {
723
-
"@types/react": "^19.2.14",
724
-
"@types/react-dom": "^19.2.3",
725
-
"@vitejs/plugin-react": "^6.0.1",
726
-
"typescript": "~6.0.2",
727
-
"vite": "^8.0.4"
728
-
}
729
-
}
730
-
```
731
-
732
-
Seven npm dependencies. The `scripts` section defines build commands — `"build": "tsc -b && vite build"` chains two separate tools. The `@types/*` packages exist because React's runtime and its type definitions are maintained by different teams.
The proxy config is the tell: `'/api': 'http://localhost:8001'` exists because the frontend and backend are separate processes. This is configuration that manages the seam between two applications pretending to be one.
752
-
753
-
**`frontend/tsconfig.json`** and **`frontend/tsconfig.app.json`** — TypeScript compiler configuration:
31 lines configuring how TypeScript compiles. Target version, module resolution, JSX transform, linting rules. Every setting is a negotiation between TypeScript, Vite, and React. Projects copy these from templates and tweak them, because getting the combination right from scratch requires understanding how three tools interact.
789
-
790
-
### What this reveals
791
-
792
-
The Jac config is one file because Jac owns the full stack. It doesn't need to negotiate between independent tools because there aren't independent tools to negotiate between.
793
-
794
-
The SOTA config is five files because five tools need to be told about each other. The `pyproject.toml` doesn't know about `package.json`. The `tsconfig` doesn't know about `pyproject.toml`. The `vite.config.ts` needs to know about both the TypeScript compiler and the backend server. Each file is a point of configuration for a tool that was designed in isolation, and the developer's job is to make them agree.
795
-
796
-
This is the polyglot tax applied to configuration. Each tool is well-designed on its own. The tax comes from the spaces between them.
797
-
798
-
**Expansion factor: ~2.3x**
799
-
800
-
---
801
-
802
615
## The Full Scorecard
803
616
804
617
| Section | Jac | SOTA | Factor |
805
618
|---------|-----|------|--------|
806
619
| Data model | 5 lines | 14 lines, 1 file | 2.8x |
The extra code isn't random — it falls into clear categories:
818
630
@@ -828,7 +640,7 @@ None of this code is *wrong*. Every line exists for a reason. The SOTA tools are
828
640
829
641
## Types Across Every Boundary
830
642
831
-
The thread that connects all five sections is **types**, and what happens to them at boundaries.
643
+
The thread that connects all four sections is **types**, and what happens to them at boundaries.
832
644
833
645
In the SOTA version, the `Todo` type exists in four representations: a SQLAlchemy class, a Pydantic model, a TypeScript interface, and (implicitly) the structure the LLM prompt describes in English. These representations are maintained independently, in different languages, by different tools. When they agree, the app works. When they drift, the bugs are silent — wrong fields, missing data, miscategorized items — because the failures happen at runtime across process boundaries where no compiler is watching.
834
646
@@ -851,7 +663,7 @@ Both versions are open source and runnable. The full code lives at [github.com/m
851
663
-**`jac/`** — The Jac version. Run with `jac start`.
852
664
-**`sota/`** — The SOTA version. Run with `./run.sh` (requires Python 3.12+, Bun, and an `ANTHROPIC_API_KEY`).
853
665
854
-
We QA-tested both with the same automated browser test suite — add via button, add via Enter, empty/whitespace rejection, XSS safety, AI categorization across all five categories, and data persistence after reload. Both pass every test. Same app, same behavior, same results. One takes 82 lines and the other takes 311.
666
+
We QA-tested both with the same automated browser test suite — add via button, add via Enter, empty/whitespace rejection, XSS safety, AI categorization across all five categories, and data persistence after reload. Both pass every test. Same app, same behavior, same results. One takes 46 lines and the other takes 233.
855
667
856
668
## What This Comparison Is and Isn't
857
669
@@ -863,4 +675,31 @@ Every era of developer tooling has eliminated a category of this kind of code. C
863
675
864
676
Jac eliminates the polyglot tax by eliminating the polyglot. One language across storage, API, frontend, and AI. It's not magic — it's a compiler that owns the full stack, so it can enforce types and generate plumbing across boundaries that no single SOTA tool can see.
865
677
866
-
The 229 extra lines aren't wrong. They're just not doing what you hired them to do.
678
+
The 187 extra lines aren't wrong. They're just not doing what you hired them to do.
679
+
680
+
---
681
+
682
+
## Bonus: The Configuration Tax
683
+
684
+
The scorecard above covers application code only. But the polyglot tax extends to project configuration too — and the ratio there tells its own story.
685
+
686
+
### Jac — 34 lines, 1 file
687
+
688
+
`jac.toml` declares everything in one place: Python dependencies, npm dependencies, dev dependencies, server config, plugin config, and LLM model selection. Both ecosystems, one file. `jac start` reads it and does the rest.
689
+
690
+
### SOTA — 78 lines across 5 files
691
+
692
+
The same concerns are spread across five files in two ecosystems:
693
+
694
+
-**`pyproject.toml`** (13 lines) — Six Python runtime dependencies: web framework, server, ORM, database driver, two LangChain packages.
695
+
-**`frontend/package.json`** (22 lines) — Seven npm dependencies. Build scripts chain two tools: `"build": "tsc -b && vite build"`.
696
+
-**`frontend/vite.config.ts`** (12 lines) — The dev proxy `'/api': 'http://localhost:8001'` exists because frontend and backend are separate processes.
697
+
-**`frontend/tsconfig.json`** + **`frontend/tsconfig.app.json`** (31 lines) — TypeScript compiler configuration negotiating between TypeScript, Vite, and React.
698
+
699
+
### The pattern
700
+
701
+
The Jac config is one file because Jac owns the full stack. The SOTA config is five files because five tools need to be told about each other. The `pyproject.toml` doesn't know about `package.json`. The `tsconfig` doesn't know about `pyproject.toml`. The `vite.config.ts` needs to know about both. Each file is a point of configuration for a tool designed in isolation, and the developer's job is to make them agree.
0 commit comments