Architectural decisions before generating code
Lino generates a lot of code quickly, but the quality of the result depends on the decisions made before generation. Choosing boundaries, project type, communication strategy, and an acceptable level of technical debt prevents initial productivity from turning into rework.
Use this page as an architecture checklist to align the team, domain, and operations before creating services, modules, and advanced features.
Project type
Before running lino project new, define the system's objective. An internal administrative project, a multi-tenant SaaS, a public API, and a distributed platform have different needs.
- Simple application: few contexts, a single deploy, and little need for operational independence.
- Modular monolith: multiple modules in the same process, with explicit boundaries and lower operational cost.
- Microservices: separate services, independent deploys, asynchronous communication, and greater operational complexity.
- SaaS: requires tenant isolation, strong security, secrets, rate limiting, observability, and clear billing/operations decisions.
This decision also defines which concerns must exist from day one. A SaaS cannot treat tenant as a late detail; a public API cannot start without rate limiting and clear contracts; a solution with events needs to consider Outbox, idempotency, and observability; an application with multiple cultures should avoid scattered strings from the beginning.
When creating the foundation, record the main choices: supported cultures, distributed cache, asynchronous communication, code analyzers, database type, web app presence, authentication, tenant, and worker. These options affect packages, templates, AppHost, parameters, secrets, generated projects, and the effort required to change the architecture later.
Modular monolith vs. microservices
The modular monolith lets you organize the domain into modules with clear internal contracts while keeping build, debug, and deploy simpler. Microservices only pay off when there are strong reasons: independent scale, separate teams, stable business boundaries, or distinct operational requirements.
Starting with a modular monolith does not mean ignoring architecture. It means preserving a simple deploy while the domain is still being discovered, while already creating internal boundaries that prevent everything from becoming a single mass of shared entities, services, and tables. That is the difference between “starting simple” and “starting without structure”.
| Criterion | Modular monolith | Microservices |
|---|---|---|
| Deploy | Simpler, usually single | Independent by service |
| Consistency | Easier to maintain local transactions | Requires events, Outbox, and eventual consistency |
| Operations | Lower infrastructure cost | More observability, queues, retries, and automation |
| Refactoring | Cheaper at the beginning | Harder once contracts have already been published |
Lino supports both paths. The decision must follow the domain and the team's operational capacity, not just technical preference. Microservices require deploy automation, distributed observability, contract versioning, network fault tolerance, messaging, reprocessing, and data governance. If the team cannot operate this safely yet, distributing too early usually increases risk.
Use a modular monolith when the priority is to learn the domain, validate the product, and keep operations controllable. Use microservices when operational independence is already a concrete need: separate scale, teams with real ownership, an independent release cycle, or incompatible infrastructure requirements between parts of the system.
Service and module boundaries
A good boundary reduces coupling and makes it clear who owns each rule. Avoid creating services per table or per screen. Services and modules should represent business capabilities.
- Catalog: products, categories, commercial information, and data queried by sales.
- Sales: orders, cart, applied pricing, snapshots, and commercial flow.
- Stock: availability, reservations, and movements.
- Security: users, roles, permissions, authentication, and tokens.
A good boundary has its own language, rules that change together, data under clear ownership, and explicit contracts for communicating with other contexts. A bad boundary usually appears as a “Common” module, an entity shared by several areas, a table queried by everyone, or a service created only because there was a new screen.
When one module needs data from another, choose between an API, integration event, shadow entity, or boundary remodeling. Direct access to another module's database is a sign of structural coupling. Even when two modules run in the same process and use the same physical database, the schema, persistence projects, and contracts must protect the business boundary.
- Sign of a good boundary: the module can evolve its entities and migrations without forcing another module to recompile because of an internal detail.
- Sign of a weak boundary: a simple change to an entity requires reviewing screens, queries, and handlers in modules that should not know that entity.
- Sign of a hidden dependency: the consumer needs to “know too much” about tables, technical fields, or the producer's internal rules.
Conscious technical debt
Technical debt is not just any imperfection. It is a decision to postpone quality while knowing the future cost. The problem is creating debt with no name, no owner, and no payment plan.
In a SaaS or modular system, the most dangerous technical debt is not just a long method. It is debt that limits evolution: modules that cannot change separately, tenant data without reliable isolation, secrets inside the repository, sensitive endpoints without limits, events without reprocessing, or integrations without a clear contract.
- Document shortcuts taken during generation and customization.
- Run build and tests before moving to the next generation stage.
- Avoid fixing generated problems with repeated local hacks; adjust the modeling or the template when the pattern is wrong.
Treat every architectural decision as auditable. If you choose not to enable messaging, record that the current flows do not depend on asynchronous processing. If you choose column-per-tenant, ensure filters and tests. If you choose synchronous integration, assume timeout, unavailability, and contract error as part of the design.
Communication strategies
Choose communication according to the real need for consistency, latency, traceability, and coupling. The main question is not “which technology should we use?”, but “does the producer need the response now, should a consumer failure cancel the operation, and who owns the data?”.
In modular systems, the most common mistake is turning communication into a hidden dependency: one module queries another module's table, reuses an internal entity, injects an external repository, or creates a synchronous call without handling failure. This seems productive at first, but makes every later change riskier because the boundary stops being explicit.
- Synchronous call: good for immediate queries and acceptable dependency. Avoid long cascades.
- Domain event: internal reaction within the same application boundary.
- Integration event: communication between modules, services, or systems, usually with Outbox.
- Shadow Entity: minimal local copy for querying and validation without depending directly on the producer.
- External HTTP integration: appropriate for third-party services, with timeout, retry, logs, and failure handling.
If the rule requires an immediate update, use a synchronous flow or revisit the boundary. If it accepts eventual consistency, prefer events to reduce coupling. When the consumer only needs to read a small subset of data from another context, a shadow entity can be safer than exposing the entire original entity.
| Question | Recommended direction |
|---|---|
| Is the response required to complete the use case? | Explicit synchronous integration, with timeout, predictable error, and a clear contract. |
| Should the producer confirm the transaction even if the consumer is offline? | Integration event with Outbox and asynchronous processing. |
| Does the consumer only need to validate or query minimal data? | Shadow Entity fed by event or integration, with local ownership of the read model. |
| Does the change happen inside the same aggregate or bounded context? | Domain event or local domain logic, without publishing an unnecessary external contract. |
