Core Concepts

Architecture style

Which of the named architecture styles Grit actually is, which ones it deliberately is not, and what it costs to move a Grit project from one to another. Written because both questions get asked in the same breath.

A word with two jobs. In Grit, architecture normally means the project shape you pick with --architecture: single, double, triple, api or mobile. This page is about something else: the architecture style the code inside is written in.

The short answer

A modular monolith with a layered API and an in-process event bus. One binary, one database, around fifty internal packages, and a domain event bus that lets the audit log, outbound webhooks and realtime clients subscribe to a write rather than be called by it.

That is the honest headline, and it is also slightly misleading, because no real system is one style. Styles compose. A Grit project is running six of them at once, each in the place where it earns its keep.

StyleWhere it sits
Modular monolithThe whole API: one binary, one deployment, around fifty internal packages.One process removes every distributed-systems failure mode from a product that does not yet have the traffic to need them.
Layered (n-tier)routes to handlers to services to models.The service owns the rules and every database call, so a job, a CLI command and a test get the same rules as the HTTP route.
Event-driven, in processinternal/events. A write emits once; audit, webhooks and realtime subscribe.The handler does not have to remember to call three systems, so adding a fourth does not mean editing every handler.
Transactional outboxinternal/outbox, behind the bus Durable delivery mode.The event is queued in the same transaction as the write, so a crash between the commit and the HTTP call loses nothing.
Orchestration (saga)internal/saga, for work that spans modules and can half fail.The sequence and its compensation are readable in one place, which is the thing choreography gives up.
Microkernel / plug-inThe framework itself: grit plugin add.Capabilities that only some projects want are not carried by the ones that do not.

The one rule the layering exists for

Handlers are thin on purpose. A handler reads the request, calls one service method and writes the answer. Every database read and write for a resource lives in that resource's service, and every service method takes a context.Context rather than a *gin.Context.

The payoff is not tidiness. It is that a rule enforced in the service is enforced in the background job, the CLI command, the import, the export and the test, not only on the HTTP route. A rule written in a handler is a rule that holds until the first time something other than a browser does the same thing.

What Grit deliberately is not

Microservices

A split is worth paying for when teams need to release independently. Until then it buys network failures, eventual consistency and a deployment pipeline, in exchange for nothing.

CQRS and event sourcing

Both earn their complexity in a hot spot, not across a codebase. The read path is already separate enough to point at another store when one query needs it.

Hexagonal / ports and adapters

The generated service talks to GORM directly rather than through a port. That is a deliberate trade: less indirection to read, at the cost of swapping the ORM being a real job.

Serverless

Background jobs, a scheduler, realtime connections and a warm connection pool all assume a process that stays up.

The whole wheel, segment by segment

The question usually arrives with a chart attached: the one with twenty-seven styles arranged in a circle. Here is Grit's position on every segment of it, so whichever one you are pointing at has an answer.

Three of the answers are partly, and that is the honest one more often than either of the others. Most styles are not a thing a system is or is not: they are a thing a system does in one place and not another.

Layered
YesLayered (n-tier)

The API itself: routes, handlers, services, models.

NoClean / Onion

The service imports GORM rather than depending inward on a port.

Component-Based
PartlyObject-Oriented

A paradigm rather than a deployment shape. Go structs with methods, no inheritance.

YesMicrokernel

The framework: a core project plus capabilities added with grit plugin add.

YesPlug-in

The same thing seen from the plugin side. Eleven modules also switch off with MODULE_<NAME>=false.

Service-Oriented
NoSOA

No enterprise bus, no coarse-grained shared contracts.

PartlyBroker

Redis carries background jobs through asynq. That is a queue, not a request bus: nothing routes calls between services.

NoMicroservices

One binary by design. The seams for a split are listed below.

NoServerless (FaaS)

Jobs, a scheduler, realtime connections and a warm pool all assume a process that stays up.

Distributed System
NoSpace-Based

The database is the system of record, not an in-memory grid.

NoPeer-to-Peer

Clients talk to one API.

Domain-Driven
PartlyDDD

It borrows the vocabulary, domain events and a service per resource, not the method: no bounded contexts, aggregates or ubiquitous-language modelling.

NoHexagonal (ports & adapters)

Deliberate: less indirection to read, at the cost of swapping the ORM being real work.

Event-Driven
YesEvent-Driven

internal/events. A write emits once, with Sync, Async and Durable delivery.

YesPublish-Subscribe

That bus is pub-sub: subscribers match a name, a resource prefix or everything, and the emitter never knows who listens.

Data-Centric
NoCQRS

One model for reads and writes, but the read path is already a separate whitelist, which is what makes adding it to one endpoint cheap.

NoEvent Sourcing

The audit chain is append-only evidence for a reviewer. State is rows, not a fold over events.

NoKappa

No streaming pipeline.

NoLambda (batch + speed)

Not a big-data system.

Concurrency
YesOrchestration

internal/saga, where a sequence spans modules and can half fail, with its compensation in one place.

YesChoreography

The other half of the same story: audit, webhooks and realtime react to an event rather than being called.

PartlyPrimary-Secondary

Reads go to DATABASE_REPLICA_URLS when you set them, and the outbox relay claims messages with a lease so another replica takes over if one dies.

PartlyPipeline / Pipe-Filter

Two places, both internal: the seeder (build, bound, batch, write) and the image pipeline (decode, orient, resize, encode).

Interpreter
NoInterpreter

The field definition is a small language, but the generator reads it once at build time. Nothing interprets a DSL at runtime.

Separation of Concern
NoMVC

A handler returns JSON; it does not pick a view. The frontend is React.

NoMVP

A desktop and Android pattern.

NoMVVM

React Query holds server state declaratively, which rhymes with it, but calling it MVVM would be a stretch.

Why Grit is organised by layer and not by feature

This is the question that follows the one above, and it is a better question. Package-by-feature, a folder per noun with that noun's model, rules and storage inside it, is the layout most Go teams land on, and it is taught as the end state of the tour for good reasons. Grit does not do it. Here is the trade, stated honestly, because it is a real one.

By layer
What Grit generates
internal/
├── models/
│   ├── invoice.go
│   ├── customer.go
│   └── ... 31 more, one package
├── services/
│   ├── invoice.go
│   └── customer.go
└── handlers/
    ├── invoice.go
    └── customer.go
By feature
A folder per noun
internal/
├── invoice/
│   ├── model.go
│   ├── invoice.go
│   └── stores/invoicesql/
├── customer/
│   ├── model.go
│   └── stores/customersql/
└── web/
    └── handlers/

What feature folders buy, and Grit gives up

A feature is a folder. Everything about an invoice is in one place and nothing else is, you can delete a feature by deleting a directory, and the root stops growing a package every time a new technology arrives. Grit pays for not having this: at forty resources, services/ is forty files and the directory no longer tells you which three of them form a subsystem. That cost is real and it arrives exactly when the codebase is big enough to need the help.

What Grit buys back: the entity graph

The examples that make feature folders look obvious have two or three domains with a clean one-way reference. A business application has thirty entities with a dense graph: an invoice points at a customer, which points at an organisation, which points back at users who raised invoices.

Go forbids import cycles. So the moment customer needs to refer back to invoice, feature folders force you into an event announcement or an interface dance to express what is really just a foreign key. That is the correct answer when two domains must stay independent, and it is a lot of machinery for invoice.customer_id.

Grit's thirty-odd models are one flat package models. Any model can reference any other with no ceremony, which is the thing that lets client:belongs_to be a single word in a generate command rather than a design conversation. The layout is not laziness; it is what buys the generator its simplicity.

The rule still bites in the other direction, and it is worth knowing before you go looking for it: models cannot import services, so a hook on a model calls the lower-level package directly rather than the service that wraps it.

The honest summary

Feature folders are the better default for an application a team writes by hand, with a handful of genuinely independent domains. Layer folders are the better default for a generated application with a dense entity graph, because one command has to know where every file goes and the entities all point at each other. Those are different problems, and the layouts are not competing for the same job.

If your application really is a few independent domains, package-by-feature is a good choice and Grit is the wrong tool, which is a better thing to find out now than in month three.

One thing worth taking from it either way

The strongest idea in the feature-folder literature is not the folders. It is that a dependency should be declared by the code that uses it and named for what it needs, rather than taken whole: a two-method interface instead of somebody else's four-method one. That is right in every layout, Grit included.

It is worth being precise about where it pays, because the obvious application of it here does not. A generated handler calls ten of its service's ten exported methods, so a “narrow” interface in front of that service would be a verbatim copy of the service: the same thing written twice, which is the cost the idea exists to avoid. Grit does not generate one, on purpose.

Where it does pay is the weaker form of the same rule: do not reach around a thing to rebuild what it already knows. The blog list handler used to skip its service and query the database directly, re-typing the search clause the service already had, so the list and the stat cards above it held two definitions of the same search in two different files. Add a searchable column to one and they disagree. The service now states that search once and exposes it as CountQuery, which the handler asks for by name. Shipped in v3.403.0.

The lesson generalises better than the interface does. Count how many methods the caller actually uses before you narrow something, and spend the effort instead on the places where a consumer has quietly grown its own copy of a rule.

A note on the name

Package-by-feature is often called domain-driven, and the folders are named after domain nouns, so the label is not wrong. It is worth keeping separate from Domain-Driven Design as a method, though, which is bounded contexts, aggregates and a language agreed with domain experts. That is a modelling discipline, not a directory layout. You can do feature folders with no DDD in them, and real DDD inside any layout on this page.

Can you refactor from one style to another?

Yes, and the answer is more interesting than yes. The thing that decides whether a refactor is a weekend or a quarter is almost never the code. It is the data.

Moving a package is easy. Moving a table out from under three foreign keys, two joins and an admin screen that lists both sides of a relationship is the actual work, and no amount of clean code makes it cheaper. So when you are weighing a refactor, look at the schema first and the package layout second.

Here are the moves people ask about, cheapest first.

Add CQRS to one endpointYesHours

The read path is already a separate whitelist of searchable, sortable and filterable columns. Point one list query at a denormalised table or a read replica and leave every other endpoint alone. This is the refactor that is almost always the right one, because it is local and reversible.

Extract one module into its own serviceYes, with careWeeks

The service layer is the seam: it is already the only thing that touches the database for its resource, and it already takes a context rather than a request. Replace the in-process call with an HTTP client and give the new service its own schema. What makes it expensive is not the code, it is the foreign keys you have to cut.

Go fully hexagonal inside the monolithPossible, rarely worth itWeeks, and it keeps costing

Putting a port in front of every service is mechanical. The ongoing cost is that the generator keeps writing the un-ported version, so every generate is followed by a hand edit and every upgrade reports a conflict. If you want this shape, the honest answer is that Grit is fighting you.

Split into microservices across the boardOnly with a reasonMonths

The reason should be organisational: separate teams with separate release cadences, or one module whose scaling profile is nothing like the rest. "It will scale better" is not a reason on its own, and a modular monolith on one large machine outruns most products for years.

Move to event sourcingNo, not as a refactorA rewrite of the write path

Current state stops being a row and becomes a fold over events, which changes every read, every migration and every admin screen. The append-only audit log is not a head start on this: it records what happened for a reviewer, it is not the source the state is rebuilt from.

Collapse existing microservices into GritYes, and often the right directionWeeks

Easier than the reverse and more common than people admit. Each service becomes a package with its own services and models, cross-service HTTP calls become function calls, and the events between them become bus subscriptions.

The seams, if you are planning to split

A Grit project has four places where a boundary can be cut without inventing one first. If you expect to split later, these are the ones to keep clean.

  1. The service. One resource, one service, all of its database access. A service that reaches into another resource's tables is the single thing that will make a future split expensive, and it is invisible until then.
  2. The event bus. A Durable subscriber already receives work at least once, through the outbox, retried until it succeeds. That is the same contract a queue between two services gives you, so turning a subscriber into a remote call changes the transport and not the semantics.
  3. The outbox. It is the thing that makes the first boundary correct rather than hopeful: the event is written in the same transaction as the data, so the two cannot disagree.
  4. Ownership scoping. Projects generated with per-record ownership or multitenancy already carry the owner through the context into every query. A split that keeps that scoping is a split that keeps its access rules.

What a heavy refactor costs you with the generator

This is the part that is specific to a framework that writes code. The further your structure moves from what the generator emits, the more often grit generate writes something you then have to adjust, and the more files grit upgrade reports as conflicts.

Nothing is overwritten: every generated file is hashed into a manifest when it is written, and a file whose hash has moved is reported rather than replaced. But a conflict is still a decision someone has to make, and twenty of them per upgrade is a tax. The practical guidance is to keep the refactor local. One module behind a port, or one endpoint on a read model, costs almost nothing. A different shape across the whole codebase means you have taken over the framework half, which is a legitimate choice as long as it is made on purpose.

If you are choosing a style for a new product

The modular monolith is not a compromise you are stuck with until you are big enough for the real thing. For most products it is the end state. It keeps one deployment, one transaction boundary and one place to look when something is wrong, and it leaves the seams where a split would go if the organisation ever needs one.

Reach past it when the reason is concrete: separate teams that cannot coordinate a release, a module whose traffic is nothing like the rest, or a compliance boundary that has to be physical. Those are reasons. Anticipated scale is not one.

The same argument, in a talk

Why the unit of generation is the resource rather than the layer, and how the framework half keeps improving after the code is yours, are the middle of the GopherCon Africa talk.

Watch that part of the deck
Community

Build alongside other Grit developers

Join the WhatsApp community for questions, tutorials, and guidance from people shipping Grit apps, and from the person who builds it. No question is too small.

Join the community