App Structure

Design the services and relationships in an Encore application

Encore uses a monorepo design: one application contains the services and infrastructure resources for your backend. Encore uses these services and their relationships to build the application model, which drives local development, infrastructure provisioning, IAM, tracing, and the Flow architecture diagram.

Keep the backend in one Encore application where practical so the model covers the whole backend. Existing systems can remain separate and connect through APIs or generated clients during an incremental migration to Encore.

Monolith or microservices

Encore supports both monoliths and microservices. A new application should generally start with one service. Direct function calls and database transactions are easier to change than interfaces between services, and directories or Go subpackages can organize the code without adding a service boundary.

Consider an application that accepts orders, reserves inventory, and sends confirmations. The first version keeps all three capabilities in an orders service.

In TypeScript, encore.service.ts defines the service:

// orders/encore.service.ts import { Service } from "encore.dev/service"; export default new Service("orders");

Encore includes the directory and its subdirectories in the service. A single-service application can place this file at the application root:

my-app/ ├── encore.app ├── package.json ├── encore.service.ts // defines the service root ├── orders.ts ├── inventory.ts ├── notifications.ts ├── db.ts └── migrations/

Use a single root-level package.json for the Encore application. Shared workspace packages that require compilation must be built before Encore parses the application; the Turborepo and Nx guides show how to configure this. See Defining services.

In Go, a package containing at least one Encore API is a service:

package orders import "context" //encore:api private method=POST path=/orders/:id/cancel func Cancel(ctx context.Context, id string) error { // ... return nil }

A single-service application can keep the service package at the application root:

my-app/ ├── encore.app ├── go.mod ├── orders.go ├── inventory.go ├── notifications.go └── migrations/

The service can contain internal subpackages, but those packages cannot define Encore APIs. See Defining services and service structs.

Order placement and inventory reservation may update the same database transaction. Keeping them in one service preserves that transaction and avoids a network call in the request path. Sending a confirmation can run after the order succeeds, but this alone does not require another service; a Pub/Sub subscriber can belong to the same service.

Add a service when the requirements change

Suppose confirmations expand into a notification capability used by orders, billing, and account security. It uses an external provider, has its own retry behavior, and must remain available when order API instances fail:

orders service ├── owns orders and inventory ├── places orders and reserves stock synchronously └── publishes order events │ ▼ notifications service ├── subscribes to order events ├── owns notification preferences and delivery records └── calls the external notification provider

The application now has one directory or package per service:

my-app/ ├── encore.app ├── package.json ├── orders/ │ ├── encore.service.ts // defines the orders service root │ ├── api.ts │ ├── inventory.ts │ ├── events.ts │ └── migrations/ └── notifications/ ├── encore.service.ts // defines the notifications service root ├── subscriptions.ts └── migrations/
my-app/ ├── encore.app ├── go.mod ├── orders/ │ ├── api.go │ ├── inventory.go │ ├── events.go │ └── migrations/ └── notifications/ ├── subscriptions.go └── migrations/

Notifications now form a cohesive capability with their own data and failure behavior. Orders, billing, and account security can all use that capability without owning notification logic.

When a service boundary helps

  • The service owns a cohesive capability with distinct operations and data.
  • Failure isolation, scaling, or security requirements differ from the rest of the application.
  • An existing external system already provides a network interface.

Keep behavior together when most changes affect both sides, one transaction must cover the operation, or neither side can do useful work alone. If the goal is only code organization, use modules, directories, or Go subpackages. Separating handlers, business-logic, and database into services would replace local calls with network calls without giving each service a distinct responsibility.

Choose how services communicate

Suppose payment processing later moves into a billing service:

  • The orders service uses a typed service API to request payment because it needs the result before confirming the order. Calls retain type safety and IDE autocomplete across the service boundary. See service calls in TypeScript and Go.
  • The orders service publishes an event for notifications because the response does not depend on delivery. Other consumers can react to the same event independently. See Pub/Sub in TypeScript and Go.

Pub/Sub workflows are eventually consistent. Messages can also be redelivered in some failure cases, including with exactly-once delivery. Make a handler idempotent when processing the same event more than once would produce an incorrect result.

Long chains of synchronous calls make a request depend on every service in the chain. Ordered events that imitate synchronous calls add eventual consistency without making the services independent.

Database changes and events

A database transaction and a Pub/Sub publish are separate operations. If both must succeed together, account for a failure between them.

Encore Go provides a transactional Pub/Sub outbox that records an event in the database transaction and publishes it after the transaction commits. TypeScript applications and Go applications not using the outbox require an application-specific consistency mechanism.

Own and share data

The orders service owns order and inventory data. The notifications service uses its API or reacts to its events instead of modifying those tables directly.

Encore also supports direct database access from multiple services through SQLDatabase.named("name") in TypeScript and sqldb.Named("name") in Go. Shared access can be appropriate for reporting, incremental migrations, or cases where another service interface adds more complexity than it removes.

A reporting service can initially read the orders database while the orders service retains responsibility for migrations and writes:

orders service ── owns schema and writes ──► orders database reporting service ── read-only access ─────► orders database

If reporting queries later interfere with transactional work, the reporting service can own a separate read model populated from order events. Reports may lag behind orders, but reporting queries no longer compete with order transactions.

Define migration ownership and write access before sharing a database. Shared schema access couples readers to schema changes even when the owning service's API remains unchanged.

Services and processes

An Encore service is a code and API boundary, while process allocation controls how services run in each environment:

  • Single process: all services run together, reducing runtime overhead and keeping service calls within one process.
  • Separate processes: each service runs independently for stronger isolation and independent scaling.

The notifications example requires separate processes in production to isolate delivery from order API failures. A development environment can still run all services in one process. Use service boundaries for application responsibilities and process allocation for runtime isolation and scaling.

Define the external interface

The orders API may be public or authenticated. Operations used only by other services can remain private.

Configure authentication and cross-origin policy at the API gateway. Encore propagates authenticated user data to service calls. The service that owns a record decides who may act on it. See the authentication guides for TypeScript and Go.

Organize larger applications

As the application adds services, a flat directory can become difficult to navigate. Services cannot be nested, but related services can be grouped into system directories:

my-app/ ├── encore.app ├── commerce/ │ ├── orders/ │ ├── reporting/ │ └── payments/ ├── identity/ │ ├── users/ │ └── auth/ └── communications/ ├── notifications/ └── templates/

A system is an organizational convention rather than an Encore resource. Moving the orders service under commerce, for example, does not change its APIs or deployment behavior.

Review a proposed boundary

  • Does the service own a cohesive application capability?
  • Which operations and schema changes does it own?
  • Which calls or events cross the boundary?
  • What does a caller do when the service is unavailable?
  • Does the caller need an immediate result?
  • Would most changes still require modifying both services?
  • Can process allocation meet the scaling or isolation requirement without another service?

To split an existing service, move the related operations and data together, replace direct calls across the new boundary with APIs or events, and define data access during the transition. Inspect the result in Encore Flow and test calls that now cross the boundary.

Public API paths do not need to change when their implementation moves to another service. See Break a monolith into microservices for a complete example.