Hello World, GoLazy

Why GoLazy exists, why the site is still marked as AI-generated, and why the framework is not trying to be neutral.

Marker illustration of a cheerful web application connecting routes, views, services, and deployment.

Published

At some point, after building enough web applications, you start recognizing the same invoice.

New product, new repo, new domain. Then the same questions arrive, wearing a different hat. Where do routes live? Where do controllers live? Are we rendering on the server or smuggling an application into the browser and calling it a page? How do assets work? Where do background jobs start? Where does configuration go? What does local development mean? How do we upgrade this thing six months from now, when everyone has forgotten why the weird bit exists?

Every project answers those questions. Many projects pretend they are not answering them. That is how you get "lightweight" systems made of twelve libraries, three build tools, a README full of rituals, and one engineer everyone is afraid to lose because they remember where the bodies are buried.

GoLazy exists because I am tired of paying that tax.

It has been around since 2022. We have used it internally on several projects, usually because we needed a small web application around something else we were building: an admin, an operational tool, a web interface for a service that did not need an entire frontend-and-backend production. For most of that time, it was useful but unfinished.

Over the last two months, we finally had enough time to polish it into something that could be shared. “Could be shared” is doing some work in that sentence. This is the first public hello, not a declaration that every corner is finished.

This first post is here to explain what GoLazy is, why I am building it, and why the website currently has a warning that the content is mostly generated by AI and has not had deep review by a human.

That warning is not decorative. It means what it says.

GoLazy is heavily in development. The framework is still in the v0.1 line, and my main focus is not polished documentation, launch copy, or convincing people to use it in production. My focus is closing the API surface and making the framework comfortable to work with every day.

That means routing, controllers, views, assets, services, release cycles, upgrade paths, and the local development loop. The boring parts. The parts that decide whether a framework becomes a useful tool or just a clever pile of packages with a logo.

Why I am building this

I am Guillermo Alvarez. I have been building software for more than 20 years, and writing Go for more than 10. I also write about engineering leadership, teams, and shipping software at cientifico.net, where I keep discovering that most software problems eventually become responsibility problems. Annoying, but convenient for recurring themes.

Before Go, a lot of my thinking about web development came from Ruby on Rails.

Rails changed how many of us thought about building web applications. It showed that development can be fast when a framework gives you a strong application shape, common conventions, and tooling that removes repeated decisions. I still believe that lesson is correct.

I do not believe Rails is the only way to build quickly.

Go has a different character. It is explicit, boring in useful ways, easy to deploy, and good at staying understandable as systems age. I like those properties. They are not glamorous. They also do not page you at 3 a.m. because the toolchain wanted to express itself.

But I never found a Go web framework that felt like Rails while still feeling like Go. Many frameworks were perfectly capable, but they came with a different structure, a different set of conventions, or a different idea of how a Go application ought to be written. I kept wanting something that used the standard library and standard Go ideas where possible, while still giving an application a coherent shape.

The Go ecosystem often leaves every application to assemble its own framework from many small pieces. That can be powerful. It can also mean every team has to rediscover the same answers, then document those answers, then enforce them, then repair them after the next refactor, then rediscover them again because apparently we enjoy seasonal suffering.

GoLazy is my attempt to put the tools I have used over the last two decades into one coherent Go application workflow.

The model

The model is simple: server first, Go underneath, conventions where repeated decisions add nothing.

GoLazy is not trying to be neutral. It has a shape.

It is for backend-rendered applications, simple mental models, strong conventions, and less frontend ceremony. It ships without choosing a database or an ORM. Business behavior belongs in services. Controllers translate HTTP. Views render the result. I think of it as service-view-controller rather than model-view-controller, and that choice deserves its own post.

Around that boundary, GoLazy provides an opinionated path for routing, controllers, views, embedded and fingerprinted assets, Tailwind, Turbo, SEO, caching, jobs, service lifecycle, development, deployment, and upgrades. Not every application needs every part. The point is that the parts share one application model instead of arriving as a bag of unrelated decisions.

Backend rendering is not a nostalgic regression. It is a reasonable next step after years of overcorrecting into single-page apps, hydration problems, client-side state synchronization, build pipelines, and frontend frameworks eating everything that was not nailed down.

Some of that complexity is real. Some of it can fuck off.

This does not mean every application should be built with GoLazy. Some products need a heavy client. Some teams need separate frontend and backend systems. Some organizations are large enough that the ceremony is not accidental; it is load-bearing.

But a lot of web work is smaller than that. You may already have a service and need a small admin. You may need to expose a useful web interface for something you have built. You may want to ship a product without first creating two applications, two deployment paths, two sets of conventions, and a peace treaty between them.

Fine. Use the tool that matches the problem.

But if you are building a product where most pages are still pages, where the backend owns the domain model, where deployment simplicity matters, and where you would rather ship the feature than build a tiny distributed systems symposium around a settings screen, GoLazy is the direction I care about.

The framework is also not optimized to win synthetic request benchmarks. Go is already fast enough for the applications I care about. I care more about the time between an idea and a production application, and whether that application is still understandable when I have to change it six months later.

That does not mean runtime behavior disappears behind developer-friendly magic. The development tools can inspect routes, services and their dependencies, jobs, assets, cache behavior, requests, logs, traces, memory, and allocations using the Go runtime's own machinery. The boring path should be visible, or it eventually stops being boring.

A project that took time

I started working on GoLazy in 2022. For a long time it was a project I kept returning to, but never had enough focused time to polish into something genuinely useful.

That has changed because of AI, but not in the way people sometimes mean it. GoLazy is not a framework generated from a prompt. A lot of code in the current version is AI-assisted, and some of it is generated, but it is based on an architecture and a set of opinions that existed before this wave of AI coding tools.

What AI has changed is throughput. Right now, a large part of my day is reviewing prompts, reading generated code, rejecting things, accepting things, and making architectural decisions. This is less glamorous than the demos make it look. It is mostly judgment, editing, and saying "no" to code that was technically valid and still a bad idea.

The challenge of building web applications in Go has not disappeared because AI can write code. If anything, the question is sharper now: what is the smallest useful shape for the application you actually need? AI can produce another frontend, another API layer, another build pipeline, and another pile of glue very quickly. It does not make those things necessary.

Some of those decisions will be good. Some will probably be bad. That is what v0.1 is for. If every decision were already obviously correct, I would be suspicious. Or lying. Probably both.

So yes, it took time. The work started in 2022, and the first useful public version is only arriving in 2026. But now it is here.

Shipping solutions, not tech

As engineers, we are responsible for shipping solutions, not technology.

That sentence is one of the main reasons GoLazy exists. The point is not to create a framework because frameworks are interesting. The point is to make it easier to turn an idea into a working product without spending the first weeks wiring the same infrastructure again.

I want GoLazy to provide the boring path: create an app, add routes, render pages, work with assets, run background jobs, test the result, deploy it, and upgrade it later. The framework should make those things feel connected without hiding the Go code underneath.

This also means GoLazy is not designed by committee. Serious contributions, critique, and disagreement are welcome. But disagreement is not the same thing as a veto. The project has a center of gravity. It will say no to some abstractions, patterns, dependencies, and architectural fashions. Not because I enjoy saying no, although it has its moments, but because a framework without taste becomes a box of spare parts.

Some technical decisions behind GoLazy need their own posts. There will be later series about why the framework is shaped this way, how the API boundaries are chosen, why some conventions exist, and where I think Go web development can be simpler.

This post is only the starting point.

Why the guides are not fully reviewed yet

The current guides are useful, but they should be read with care. They are part of the development process, not the final promise.

Right now, I am using AI heavily to help document the moving target while I work on the framework itself. That is practical, but it has a cost: generated documentation can be incomplete, too confident, or slightly behind the code. Documentation that sounds confident while being subtly wrong is one of software's more annoying little gifts.

Until the API surface settles, repeatedly doing a deep human review of every guide would take time away from the work that makes the guides true. Reviewing a guide for an API I might change next week is not discipline. It is theater.

The plan is to treat v0.2 as the point where that changes. Once GoLazy reaches that milestone, the guides should be human reviewed, the core workflow should be stable enough to test seriously, and the framework should be ready for more people to try with clearer expectations.

Until then, the warning stays.

If you are reading the site today, treat it as a public workbench. The project is real. The code is moving. The ideas are being tested in the open.

The promise is not that everything is finished. The promise is that GoLazy is being shaped toward a practical way to build and ship Go web applications without carrying more machinery than the product needs.

I Care More About Developer Speed Than Request Benchmarks

Why GoLazy optimizes the path to a maintainable production application without hiding runtime behavior.

Marker illustration of a developer moving from an idea to a deployed web application while leaving a stopwatch behind.

Published

Web framework benchmarks are excellent at answering a question almost nobody asked while choosing a framework.

How many trivial responses can this router return per second on hardware your application will not use, without a database, template, authorization check, network call, cache miss, log line, or business decision getting in the way?

The answer can be interesting. It can reveal terrible implementation choices. It can find allocation problems and accidental bottlenecks. It is also rarely the reason a product ships late.

Most applications do not fail because the router could only serve an absurd number of requests per second instead of a more fashionable absurd number.

They fail because development became expensive.

The benchmark I care about

I care about the time between an idea and a production application.

Not the demo. Not hello world on localhost. Production: assets have permanent URLs, pages have metadata, jobs run somewhere, cache behavior is visible, configuration has a home, deployment is repeatable, and six months later the application can be upgraded without archaeology.

That path is the GoLazy benchmark.

How quickly can a developer answer these questions?

  • Where does this route go?
  • Where does the business operation live?
  • Which services does it depend on?
  • Which template rendered this fragment?
  • What happened during this request?
  • Why did the cache miss?
  • Which asset is the browser actually loading?
  • What will happen when this process is deployed?

A framework that returns a response in fewer nanoseconds but leaves every application to invent those answers is optimizing a component. I want to optimize the work.

Opinionated means the pieces know each other

GoLazy does not only provide a router and renderer, then wish the application good luck.

It has a conventional path for controllers, views, services, embedded and fingerprinted assets, Tailwind, Turbo and Stimulus, SEO, caching, background jobs, service lifecycle, development, deployment, and upgrades.

That list is not valuable because more features are automatically better. A framework can become an incoherent warehouse very quickly.

The value is that the pieces share an application model.

The route table knows which controller and action will run. The controller resolves application services. The renderer knows which view and layout are involved. The asset registry knows the embedded output. Jobs and services participate in application startup and shutdown. Development tools can inspect those boundaries because the framework helped create them.

Opinionated should mean the pieces cooperate. Otherwise it only means the documentation contains strong adjectives.

Visibility is part of developer experience

Developer-friendly frameworks are sometimes accused of hiding too much. The accusation is often deserved.

A convention is useful when it removes a repeated decision. It becomes dangerous when it removes the developer's ability to see what happened.

GoLazy's development tooling is built around that distinction. The development panel can inspect routes, service state and dependencies, jobs, assets, cache entries, requests, logs, build information, and runtime details.

Per-request tracing uses Go's own runtime machinery. A request can be broken into routing, middleware, controller construction, actions, views, layouts, and partials. The trace artifacts include memory deltas and allocation counts. The point is not to build a proprietary performance religion. The point is to make the normal Go evidence available at the level where an application developer is asking a question.

If a view is slow, inspect the view region. If an action allocates too much, inspect the request. If a service dependency is missing, see the graph. If the app does not compile, the development panel should remain available instead of disappearing with the process it was supposed to explain.

That is developer experience too.

Go is already fast

Choosing developer speed does not require choosing a slow runtime.

Go gives us an excellent baseline: compiled binaries, cheap concurrency, a strong standard library, useful profiling and tracing tools, straightforward deployment, and performance that is more than sufficient for a large class of web applications.

GoLazy should not waste that advantage. Framework overhead matters. Allocations matter. Request latency matters. Regressions should be measured and fixed.

They are constraints, not the product.

If an application reaches the point where GoLazy's controller dispatch or view rendering is the dominant production bottleneck, that is a good problem to investigate with evidence. Designing every application around a synthetic router benchmark before it has users is performance cosplay.

The actual tradeoff

An opinionated workflow reduces local choice.

GoLazy tells you where services live. It has a preferred controller and view model. It has an asset path, a development command, and an application lifecycle. If you want to replace every piece independently, the framework will probably feel restrictive.

That restriction is intentional. Repeated freedom becomes repeated integration work, repeated documentation, and repeated disagreement. GoLazy spends some flexibility to make the common path coherent.

It is not the right trade for every team. Some organizations need independently owned frontend and backend systems. Some products need substantial client-side state. Some infrastructure has requirements a full-stack framework should not own.

Use the architecture the problem requires.

But when the product is a server-rendered application and the team would benefit from one understandable path from request to production, I do not care whether another router wins by twelve nanoseconds.

I care whether we can ship the feature, understand it, and change it later without summoning the engineer who remembers where the bodies are buried.

Why GoLazy Has No ORM

LLMs made SQL cheaper to write, but they did not make business modeling less important.

Marker diagram showing a direct path from a database to business services without an abstraction maze.

Published

For years, one of the practical arguments for an ORM was that nobody wanted to write the same database plumbing again.

Define a model. Declare a few associations. Ask it a question in the host language. Let the framework turn that into SQL. The abstraction was not only elegant; it removed work that developers were tired of doing by hand.

Then LLMs became extremely good at writing SQL.

Not flawless. Not magically aware of your indexes, production cardinality, locking behavior, or the query plan waiting to ruin Friday evening. But very good at producing the first useful query, the scanning code, the migration, and the tests around it.

The economics changed.

Writing SQL is cheaper now. Understanding the business is not.

The valuable part was never only query generation

It would be stupid to conclude that Active Record no longer matters because a model can write a JOIN.

The valuable part of a good Rails model is not merely that it saves SQL keystrokes. It gives the business a vocabulary. A model can gather state, validation, associations, and behavior into something a developer can read and reason about.

Open a good model and you can learn what the application believes an account, subscription, invoice, or publication actually is. That remains valuable whether the query was written by a person, generated by an ORM, or suggested by an LLM.

The problem is not modeling.

The problem is making the persistence abstraction the mandatory center of the business architecture.

Abstraction has a maintenance cost

An ORM lets developers avoid SQL until it does not.

Eventually a query matters enough that somebody must understand what was generated. A relation performs poorly. Preloading changes the result shape. A callback runs at an unexpected moment. A transaction boundary is not where the business operation needs it. A feature crosses several models and an external API, so the interesting behavior moves into a service object anyway.

At that point the application pays for two things:

  • the database behavior itself;
  • the abstraction used to avoid seeing the database behavior.

That tradeoff can still be worth it. Rails proves that it often is. GoLazy simply refuses to make it a framework requirement.

Go does not offer Ruby's dynamic Active Record experience naturally. There are capable Go ORMs and code generators, but each makes different choices about schema ownership, generated types, query APIs, migrations, relationships, and runtime behavior. Choosing one would force every GoLazy application to accept a large architectural decision that has nothing to do with rendering a web page.

So GoLazy does not choose one.

Let services own persistence

In a GoLazy application, services own business behavior and data access.

A service may use SQLC-generated queries:

type Service struct {
	queries *store.Queries
}

func (s *Service) Publish(ctx context.Context, id int64) (Post, error) {
	// Check the business rules, persist the change, and return a domain result.
}

Another service may use an ORM. Another may call an API. The controller does not care:

func (c *PostsController) Publish(id int64) error {
	post, err := c.posts.Publish(c.Request().Context(), id)
	if err != nil {
		return err
	}
	return c.RedirectTo("/posts/"+post.Slug, http.StatusSeeOther)
}

The exact APIs will vary, but the ownership does not. The controller translates the request. The service performs the business operation. Persistence stays behind that boundary.

GoLazy can initialize a database pool, run embedded migrations, expose service health, and provide durable backends for jobs or storage. Those are application mechanics. It does not decide that your posts table is your business architecture.

LLMs remove toil, not judgment

LLMs strengthen this choice because they are useful exactly where explicit database code used to feel disproportionately expensive.

They can draft:

  • schema migrations;
  • SQL queries;
  • SQLC definitions and Go call sites;
  • scanning and mapping code;
  • fixtures and integration tests;
  • boring repository methods when a repository is actually useful.

That makes direct, inspectable code more affordable.

It does not make that code correct by divine intervention. The developer still owns constraints, transactions, query plans, indexes, authorization boundaries, migrations, and production behavior. Generated SQL must be reviewed like generated Go.

More importantly, an LLM cannot decide what the business should mean. It can reproduce patterns. It can suggest names. It can confidently put a rule in the wrong layer with excellent formatting.

The scarce skill is no longer typing the query. The scarce skill is knowing which operation the application should expose, which invariants it must preserve, and where that responsibility belongs.

That is service design.

No ORM is not an ORM ban

GoLazy does not ship an ORM. It does not prohibit one either.

If Ent gives your application the right schema workflow, use Ent. If GORM makes the team productive, use GORM. If SQLC plus PostgreSQL fits, use that. If SQLite and handwritten queries are enough, congratulations on avoiding a meeting.

The framework does not need to win that argument.

Its job is to give the application a stable web shape around whichever persistence choice the business requires. The service boundary lets that choice remain local instead of turning it into the identity of the entire framework.

LLMs made SQL cheaper. They did not make domain modeling obsolete. If anything, they made the distinction harder to ignore.

Code is becoming abundant. Knowing what the code is supposed to do is not.

Why GoLazy Uses SVC Instead of MVC

Why GoLazy puts business behavior in services instead of splitting it between database models and service objects.

Marker diagram connecting services, views, and controllers while crossing out an ambiguous tangle.

Published

There is a moment in the life of many Rails applications when somebody creates app/services.

The directory is not part of the original story. The story was model, view, controller. Controllers handle the request. Views render the result. Models contain the business logic. Fat models, skinny controllers. Nice, memorable, and genuinely productive.

Then the application grows.

Creating an invoice now means loading an account, checking a subscription, applying regional tax rules, reserving a sequence number, writing several records, scheduling a job, and sending an email. None of those steps is especially strange. The strange part is deciding which one of the models owns the operation.

So somebody creates CreateInvoiceService.

Nothing explodes. The application may even improve. But it now has two answers to a basic question: where does the business logic live?

Some of it lives in models. Some of it lives in services. The distinction is usually explained with words like "local behavior" and "orchestration," which sound precise until three people have to decide where the next rule belongs.

The services directory is trying to tell us something.

Active Record is valuable

Rails' Active Record model is one of the most productive abstractions in web development. It gives the developer a place where persisted data and business language can meet. Open a well-designed model and you can understand a surprising amount about the application.

That is priceless.

It is also easy to caricature. Active Record is not bad because it knows about the database. A model with meaningful behavior is often much better than a collection of anemic structs passed through procedural code. The ability to ask an object a business question can make code readable in ways that a repository full of generic CRUD methods never will.

The problem appears when the model is expected to be the center of all business behavior, including behavior that does not have one natural model.

A business operation may cross five persisted types. It may call an external system. It may not write to a database at all. It may coordinate a transaction and then schedule work that must happen outside it. Choosing one Active Record model as the owner can become arbitrary. Putting the operation in a service is reasonable, but now business behavior has two homes.

That split is not catastrophically bad. Plenty of good Rails applications use service objects. The side effect is architectural ambiguity: developers must continually decide whether a rule belongs inside a model or inside the service coordinating several models.

GoLazy avoids that decision by removing the model from the framework architecture.

Service, view, controller

GoLazy applications have three main boundaries:

  • Services own business rules, use cases, domain types, data access, and domain-facing integrations.
  • Controllers translate HTTP into service calls, then choose a response, status, redirect, or view.
  • Views present the result.

That is service-view-controller: SVC.

The service is not necessarily a network service. It does not need to be a microservice, a daemon, or a process with a logo and its own Kubernetes namespace. It is an application boundary expressed as a Go package.

A service can be small. It can contain a few functions and types. It can use PostgreSQL, SQLite, an API, embedded files, memory, or all of them. It owns a piece of the business and exposes operations in the language of that business.

The controller should not know how the data is stored. It should know that it can create an invoice, publish a post, register a vote, or load an account summary. HTTP is the controller's problem. The business operation is the service's problem.

The view should not discover business rules by calling the database from a template. It receives the result and renders it. This is not revolutionary. That is rather the point.

The database is an implementation detail

GoLazy ships without a database and without an ORM.

That does not mean database work is unimportant. It means the framework does not know enough about your business to choose its persistence model.

Your application may use SQLC and PostgreSQL. It may use Ent, Bun, GORM, or handwritten SQL. It may talk to several databases. It may mostly call another service. It may not need persistent data at all.

Whatever you choose belongs behind the service boundary. GoLazy can help initialize dependencies, run migrations, manage jobs, and expose development information, but it does not pretend that a database row is automatically the domain model of every application.

This is your fucking problem.

I mean that positively.

It is your business. You know the invariants, the operations, the ugly exceptions, and the vocabulary better than a web framework ever will. A framework should provide a coherent place for that knowledge. It should not manufacture a generic business architecture and congratulate itself for saving you from understanding your own product.

What the framework does own

This does not make GoLazy unopinionated. Quite the opposite.

GoLazy has strong opinions about the web application around the services: routing, controllers, views, server rendering, assets, Turbo, Tailwind, SEO, caching, jobs, development tooling, deployment, and upgrades.

Those are repeated application problems. Solving them coherently is the framework's job.

The rules for whether an invoice can be cancelled are not repeated framework problems. They are business rules. Solving them is the application's job.

That boundary matters. "Bring your own everything" is not a framework philosophy; it is a shopping list. But claiming ownership over the business model is not necessary to provide an opinionated application workflow.

GoLazy draws the line at services.

What we lose

SVC has costs.

You do not get one framework object that automatically provides persistence, validation, associations, callbacks, dirty tracking, and business behavior. You must design service APIs. You must decide what your domain types mean. You must choose how data crosses the persistence boundary.

For a small CRUD application, an Active Record model can be wonderfully efficient. SVC may look like extra structure when the entire business really is close to a few database tables.

That is fine. GoLazy is not proof that Rails is wrong. Rails remains exceptionally good at the model it chose.

GoLazy chooses a different center because Go is a different language and because I want business behavior to have one obvious home as applications grow.

Not the controller. Not the view. Not half in a persisted model and half in a coordinating object created three years later.

The service owns the business. Everything else is an adapter.