honest framework

Why should I care?

Less pain. No new pain. The numbers prove it.

Most frameworks ask you to accept new pain in exchange for solving old pain. A new abstraction, a new mental model, a new set of things that can go wrong. Honest Framework asks for none of that. What it asks is smaller: write data instead of classes, write functions instead of methods, and let the framework read what you already wrote.

In return, you stop debugging a specific category of bugs entirely.

These are not hypothetical. They are the bugs React developers file tickets about every sprint.

useState and the server get out of sync. The component says one thing. The database says another. You write a useEffect to reconcile them, which introduces a loading state, which introduces a flash, which requires another flag to suppress it. With DATAOS, the DOM is the state. There is nothing to synchronize because there is no second copy.

A LaunchDarkly flag has three states and your switch has two. The third state was added last quarter. The fallback path runs for a subset of users who hit the missing branch. No error is thrown. With honest-features, the handler table must cover every state in the vocabulary. A missing entry fails immediately, not silently.

A Prisma migration conflicts on merge. Two branches each add a migration. The file names collide or the order is wrong. Someone spends the morning sorting it out. With honest-persist, there are no migration files. Each branch carries its schema declaration. apply() computes the diff against the live database. Merge day is the same as any other day.

You add a new valid value and forget to test it. The test suite covers the original values. The new one ships untested. With honest-test, the tests come from the vocabulary. Add a value, the test appears automatically.

Redux DevTools shows you what changed but not why. You can see the state transition. You cannot see which server call caused it, which database query ran, or whether it hit a cache. With honest-observe, browser events join to server events join to database events on a single request_id. The full trace is one query away.

These are not hypothetical. They are the bugs Rails developers work around every week.

A migration conflict on merge day. Two developers each add a migration. The version numbers collide or the order is wrong on the target environment. Someone writes a compensating migration. With honest-persist, there are no files to conflict. Declare the schema you want; the diff is computed against what exists. Merge day is the same as any other day.

A Flipper flag grows a third variant and your case statement has two. The new variant hits a missing branch. No error. Wrong behavior ships. With honest-features, the handler table must cover every declared state. A missing entry raises immediately at dispatch time.

before_action chains that are hard to follow. A controller action runs through five before_action filters. One mutates state that a later one reads. A new developer adds a sixth. Something breaks in a way that is hard to reproduce. With honest code, I/O stays at boundaries and functions stay pure. The chain is flat and visible.

You add a new valid parameter value and the tests still pass. The existing tests cover the original values. The new one is untested. Nothing tells you. With honest-test, the tests come from the vocabulary. Add a value, the test appears.

Sentry shows you the exception but not the path that caused it. You know what crashed. You do not know what sequence of requests, queries, and state changes led there. With honest-observe, the full execution trace — browser through controller through database — is queryable by request_id.

These are not hypothetical. They are the bugs Django developers recognize immediately.

makemigrations produces a migration that conflicts on merge. Two branches each generate a migration. Django detects the conflict. Someone resolves it by hand, sometimes incorrectly. With honest-persist, there are no generated files. Declare the schema you want; apply() diffs it against the live database. Branches never conflict on schema.

A Waffle flag gets a third state and your if/else has two branches. The third state hits the else. Wrong behavior runs. No exception is raised. With honest-features, the handler table must cover every state in the vocabulary. A missing entry fails immediately.

A Django form validator that grows without its tests growing with it. You add a new valid value to a choices field. The existing tests still pass. The new value ships untested. With honest-test, the tests come from the vocabulary declaration. Add a value, the test appears automatically.

Middleware that mutates request state in ways that are hard to trace. A middleware adds data to request. Something downstream reads it. A new developer adds another middleware that overwrites it first. The bug is ordering-dependent and hard to reproduce. With honest code, data flows explicitly as function arguments, not as request-level side effects.

Sentry captures the exception but the trail goes cold before the database. You see the Python traceback. You do not see the query that preceded it, the session state that contributed, or the browser action that triggered the request. With honest-observe, browser, server, and database events share a request_id and are queryable as a single trace.

These are not hypothetical. They are the bugs FastAPI developers encounter in production.

A Pydantic model validates the shape but not the business rules. `sort: str` accepts anything. You add `Literal["name", "date"]` to constrain it. Six months later someone adds "status" to the Literal but forgets to add a test case for it. The new path ships untested. With honest-type, the tests come from the vocabulary. Add a value, the test appears automatically.

An environment variable flag has no enforcement. `FEATURE_NEW_CHECKOUT=true` in production, `false` in staging. Someone adds a third behavior tied to `"canary"`. The if/else has two branches. The canary path hits the else and runs the wrong handler. With honest-features, the handler table must cover every declared state. The missing entry fails immediately.

Alembic revision chains conflict on merge. Two branches each generate a revision. The head pointers diverge. Someone resolves the merge_heads manually. With honest-persist, there are no revision files. Each branch carries its schema declaration. apply() computes what the live database needs.

Dependency injection hides what a function actually needs. A route handler depends on five injected services. Tests require mocking all five even when only one is relevant. With honest code, pure functions take their dependencies as plain arguments. The test is: call the function, check the result. No mocks.

Structured logs exist but joining browser to server requires custom tooling. You have server logs. Your frontend sends events to a separate analytics system. Correlating a user action to the server call it triggered to the query that ran requires manual work. With honest-observe, all three share a request_id and live in the same log.

These are not hypothetical. They are the bugs Go developers write postmortems about.

A switch on a string constant that someone extends without updating. A new state is added to an iota or a string constant set. The switch has no default or the default silently continues. Wrong behavior runs without a compile error or a panic. With honest-features, the handler map must cover every declared state. A missing key panics immediately at dispatch, not later in production.

golang-migrate files that get out of order between branches. Two developers each add a numbered migration file. The numbers conflict or the order of operations is wrong on the target schema. Someone writes a compensating migration. With honest-persist, there are no numbered files. The schema declaration is diffed against the live database. Merge day is unremarkable.

A struct that carries state across calls without declaring it. A method on a struct reads a field that was set by a previous call. The method's behavior depends on call order. Tests pass in isolation but break in integration. With honest code, functions take data in and return data out. No hidden state. The function behaves identically regardless of what ran before it.

slog tells you what happened but not why. The log line shows the error. It does not show the sequence of calls that produced it, the database queries that ran, or the user action that triggered the request. With honest-observe, the full trace — from incoming request through every function through every query — is queryable by request_id.

Build tag flags that require a rebuild to toggle. Feature flags controlled by build tags mean a new binary for every flag state change. With honest-features, state changes via a signed API call. No rebuild. No redeploy.

These are not hypothetical. They are the bugs Laravel developers deal with on every team.

Migration conflicts on merge. Two branches each add a migration with sequential timestamps. The order matters. On merge, the schema ends up in an unexpected state. Someone writes a fix migration. With honest-persist, there are no files to conflict. Declare the schema you want; the diff is computed against what the database actually has.

A config flag grows a third value and the match has two cases. The new value hits no matching case. The default runs. Wrong behavior ships without an exception. With honest-features, the handler table must cover every declared state. A missing entry fails loudly, immediately.

Eloquent's lazy loading fires queries you did not ask for. A relationship is accessed on a model that was not eager-loaded. A query fires inside a loop. N+1 shows up in Telescope hours after the code shipped. With honest-persist, queries are explicit data structures. The executor runs exactly what you asked for, nothing more.

Middleware that writes to the request and something downstream reads it unexpectedly. A middleware sets a value on the request object. A controller reads it. A new middleware added later overwrites it first, in a different order than intended. The bug is position-dependent. With honest code, data flows as explicit function arguments, not as shared request state.

Telescope shows you the query but not what triggered it. You see the slow query. You see the exception. You do not see the browser interaction that preceded the request or the session state that contributed. With honest-observe, browser, server, and database events are joined by request_id and queryable as a single trace.

These are not hypothetical. They are the bugs that survive Rust's type system because they live in logic, not in types.

A match on an enum that compiles but mishandles a new variant. You add a variant to an enum. The exhaustive match catches it at compile time — good. But the new arm dispatches to the wrong handler because the handler table was not updated consistently everywhere the enum is used. With honest-features, the vocabulary is the single source of truth. Add a state once; every dispatch table that fails to cover it fails immediately.

Migration files that diverge between branches. Even in Rust projects, schema migrations are file-based. Two branches add migrations. The order of application matters. Someone resolves a merge conflict in a migration file incorrectly. With honest-persist, the schema is a declaration. The diff against the live database is computed. No files to merge.

Struct methods that hide state transitions. A method on a struct reads fields set by a previous call. The compiler cannot tell you the call is order-dependent. Tests pass in isolation. Integration breaks. With honest code, functions take data in and return data out. The compiler sees all dependencies because they are all in the function signature.

Instrumentation that lives outside the function it tracks. You add a timer around a function call. You add a log line before and after. The function changes; the instrumentation does not. With honest-observe, the framework instruments every boundary automatically. You write no logging code.

Test coverage that does not cover all enum variants. You test three out of four variants. The fourth ships untested because nothing enforces total coverage. With honest-test, the vocabulary drives the test suite. Every declared value is a test case. Coverage is total by construction.

These are not hypothetical. They are the bugs that appear in every codebase at scale, regardless of language.

Dispatch on a value that has more states than the handler covers. A flag, a status field, an event type. Someone adds a new value. The conditional has no branch for it. The fallback behavior is wrong. No error is raised. This pattern appears in every language. With honest-features, the handler table must cover every declared state. A missing entry fails immediately.

Migration files that conflict between branches. Sequential numbering, timestamp-based naming, or any other file-based migration scheme creates ordering conflicts when two developers work on schema changes in parallel. With honest-persist, there are no files. The schema is a declaration. The diff is computed against the live database. Branches never conflict on schema.

Tests that cover the values that existed when the test was written. A new valid value is added. The tests do not cover it. Nothing enforces coverage. The new path ships untested. With honest-test, tests are derived from vocabulary declarations. Add a value, the test appears. The coverage is always total.

Functions with hidden dependencies on call order or external state. A method reads a field set by a previous call. A function reads a global. The behavior is order-dependent. Tests pass in isolation. Integration breaks. With honest code, pure functions take all dependencies as arguments and return all outputs as values. The behavior is deterministic regardless of context.

Logs that answer "what" but not "why." You know the error occurred. You do not know the sequence of events — browser interaction, server processing, database queries — that produced it. With honest-observe, everything is in one append-only log joined by request_id. The full trace of any user interaction is one query.

These are the failure modes that appear even in codebases that try to be functional, because the framework surrounding the pure core is not honest.

A partial function at the dispatch layer. The handler table covers three of four flag states. The fourth hits a runtime error or a silent default. In a total functional language this would be a type error. In most production systems it is not. With honest-features, the vocabulary defines the domain and the handler table must be total over it. The framework enforces this at construction time.

Schema migrations as a history of instructions rather than a declaration. Alembic, Flyway, Active Record — all of them define schema as a sequence of imperative steps. apply() is not idempotent. The current schema is whatever the history produces. With honest-persist, the schema is a value. The migration is a diff function: pure, idempotent, and deterministic.

Tests that sample from an infinite space. QuickCheck is stronger than example-based testing. It is still probabilistic over an unbounded domain. If the valid values for a field are a finite set, exhaustive enumeration is stronger than sampling. With honest-test, finite vocabularies produce exhaustive test suites. The total function property is verified, not sampled.

I/O that lives inside the pure core because the framework put it there. A route handler that validates, queries, and renders in one function. Mocks required to test any of it. With honest code, I/O lives at declared boundaries. The pure core has no I/O. It has no mocks. It has one assertion: call the function, check the result.

Observability wired by hand at every boundary. Even functional codebases log manually. The IO monad makes I/O explicit but does not make it automatic. With honest-observe, the framework emits events at every boundary. The developer writes no instrumentation code.

The numbers

The claim that pure functions are slower than object methods is false. It is false in every language where it has been measured.

The benchmark measures the same business logic implemented two ways: dishonest (class-based, method dispatch, mutable state) and honest (pure function, plain data). Same computation. Same hardware. 1000 runs each.

LanguageClass-based (ns)Pure function (ns)Speedup
Java3961253.2x faster
Go155801.9x faster
TypeScript11037921.4x faster
Python6245421.15x faster
PHP10218331.2x faster
Ruby8547501.14x faster

Every language. Every time. The pure function is faster.

The Java number is not a fluke. Method dispatch goes through the vtable. Field access follows pointers across the heap. The CPU fetches data from scattered memory locations. A pure function receives its arguments directly, operates on stack-local data, and returns. No pointer chasing. No vtable lookup. The hardware prefers it.

The full methodology, source code, and Docker image to run it yourself are at honestcode.software/evidence.html. The numbers came from a real test harness, not a theoretical model.

What you are not giving up

Honest Framework is not a new language. It is not a new runtime. It is not a framework that replaces your web server or your database or your deployment pipeline.

You keep whatever you use to handle HTTP. You keep your database. You keep your existing template engine. Honest Framework adds a layer on top: a type classifier at the boundary, a schema diff instead of migration files, a flag vocabulary and handler table instead of if/else, an event log instead of scattered logging calls.

The code looks slightly different. A dict where a class used to be. A function where a method used to be. A vocabulary declaration where a schema object used to be. The difference is visible in a code review. It is not visible to your infrastructure. Within a few weeks this will look normal, and the old way will start to look like the smell it always was.

What you gain is structural. The type system enforces itself. The tests generate themselves. The linter reads the same declarations the runtime reads. The log needs no configuration. The schema needs no file chain.

The question is not whether to accept new complexity. There is no new complexity. The question is whether to stop carrying the old complexity that these patterns eliminate.