For a small team, a modular monolith is usually enough: one deployable application with strict internal boundaries around business capabilities. Split a part into its own service only when you can name a concrete problem, such as a component that must scale independently, a release that keeps getting blocked, or a team that cannot work without stepping on others. Microsoft's own guidance lists real benefits for microservices, and it lists real costs that a small team pays every day.
Who this is for and what you will have at the end
This guide is for developers, tech leads and technical founders on teams of roughly two to fifteen engineers who are asking whether to build or move to microservices. You do not need prior domain-driven design experience.
By the end you will have a way to compare the two styles, a list of signals that justify a split, a set of rules for keeping a monolith modular, and a step-by-step path for extracting a service later if you need to. The guidance comes from the Azure Architecture Center on Microsoft Learn. Team-size thresholds are not given there, so where this article gives a rule of thumb it says so.
What the two styles actually are
A monolith is an application deployed as a single unit. Every feature ships together, and the code usually shares one process and one database. Microsoft's n-tier guidance notes that a monolithic design prevents independent deployment of features, which is the core limitation people run into at scale.
Microservices are a different style. The Azure Architecture Center describes them as "small, independent, and loosely coupled components that a single small team of developers can write and maintain." Each service is a separate codebase, can be deployed on its own, persists its own data, and talks to other services through well-defined APIs. Each one should implement a single business capability within a bounded context.
A modular monolith sits between them. It is still one deployable, but the code is split into modules that mirror business capabilities, and modules talk through explicit interfaces. This is a common practitioner term rather than a separate Azure architecture style, so treat the Azure pages as the source for the monolith and microservice sides, and the modular rules below as design guidance that applies the same domain principles inside one process.
| Aspect | Monolith (unstructured) | Modular monolith | Microservices |
|---|---|---|---|
| Deployment unit | One | One | One per service |
| Code boundaries | Often tangled over time | Modules with explicit interfaces | Separate codebases |
| Data | Shared database, shared tables | One database, tables owned by one module | Each service owns its data |
| Failure of one part | Can take down the process | Can take down the process | Can be isolated if callers handle faults |
| Operational load | Lowest | Low | Highest |
| Team skills needed | General | General plus discipline | Distributed systems skills |
What Microsoft says microservices give you
The Azure Architecture Center lists these benefits:
- Agility. You can update a service without redeploying the whole application and roll back if something goes wrong. In a traditional application, a bug in one part can block the entire release process.
- Small, focused teams. A microservice should be small enough that a single feature team can build, test and deploy it.
- Small code base. Services do not share code or data stores, which avoids the tangled dependencies that build up in monoliths.
- Mix of technologies. Teams can choose the stack that suits a service.
- Fault isolation. If one service goes down, the application keeps working as long as upstream services handle faults, for example with the Circuit Breaker pattern or asynchronous messaging.
- Scalability. You can scale a hot subsystem without scaling everything.
- Data isolation. A schema change affects one service instead of every component that shares the data.
Read that list as a set of problems that appear at a certain size. Independent deployment helps when many teams are blocked on each other. Independent scaling helps when one part has very different load. If you do not have those problems yet, you are paying for benefits you cannot use.
What Microsoft says it costs
The same page lists the challenges, and most of them land on a small team:
- Complexity. There are more moving parts. Each service is simpler, but the whole system is more complex. You must handle service discovery, data consistency, transaction management and interservice communication.
- Development and testing. Existing tools are not always designed for service dependencies, refactoring across service boundaries is difficult, and testing dependencies is hard while the application is evolving quickly.
- Lack of governance. Too many languages and frameworks can make the application hard to maintain.
- Network congestion and latency. Long chains of service calls add latency, and chatty APIs hurt.
- Data integrity. With each service owning its data, a change spanning several services is unlikely to be a single ACID transaction. Microsoft says the approach is closer to eventual consistency and recommends embracing it where possible.
- Management. A successful microservice architecture requires a mature DevOps culture, and correlating logs across services for one user operation is difficult.
- Versioning. Updates to a service must not break the services that depend on it.
- Skill set. Microservices are highly distributed systems, and Microsoft tells you to evaluate whether the team has the skills and experience.
The page also lists antipatterns. The first is implementing microservices without a deep understanding of the business domain, which leads to poorly aligned service boundaries. A young product with shifting requirements is exactly the situation where the domain is not yet understood.
A decision framework for a small team
Use this sequence. It is a practical rule of thumb built on the Microsoft points above, not a Microsoft-published algorithm.
Step 1: Name the problem you are trying to solve
Write down the specific pain in one sentence. Good examples map directly to the benefits above: "Image processing needs ten times the compute of everything else", "A bug fix in billing cannot ship because the reporting module is mid-change", "The payments component must meet a stricter availability target." If the sentence is "microservices are modern" or "we might need to scale someday", stay with the monolith.
Step 2: Check whether a cheaper fix exists
Before splitting, ask whether the problem has a smaller solution:
- Heavy background work can often move to a queue and a worker process while the code stays in the same repository. The pattern is covered in Queue-based load leveling with Azure Service Bus.
- Scaling pain can often be handled by running more copies of the whole application, if it is stateless.
- Release collisions can often be reduced with feature flags and better module boundaries.
Step 3: Check the readiness list
Microsoft says microservices need a mature DevOps culture and strong operations. Before you commit, confirm you can answer yes to each:
- Can you deploy any part of the system through an automated CI/CD pipeline without manual steps?
- Do you have centralized logging, metrics and distributed tracing, so a single user request can be followed across services?
- Can the team handle eventual consistency in the business process, instead of relying on one database transaction?
- Is someone available to own on-call and incident response for several services?
- Do you understand the domain well enough to draw stable boundaries?
If several answers are no, the extra services will cost more than they return.
Step 4: Split along business capabilities
If you pass the checks, use domain analysis. Microsoft's guidance is to design microservices around business capabilities, not horizontal layers like data access or messaging, and to aim for loose coupling and high functional cohesion. A service is loosely coupled if you can change it without updating others at the same time. It is cohesive if it has a single, well-defined purpose.
How to build a modular monolith well
If your decision is to stay, the aim is to keep the option of splitting later. These rules apply the Microsoft domain principles inside a single deployable.
- Draw boundaries by business capability. Use the domain-analysis approach: map business functions and their connections with domain experts, look for closely related functions, and separate core subdomains from supporting and generic ones. Microsoft notes that building a shared understanding of the domain should come before choosing technologies.
- Give each module its own model. Microsoft's bounded context guidance explains that one real-world entity, such as a drone, needs different attributes in different contexts. Do the same: an
Orderin billing is not the same class as anOrderin fulfillment. - Expose a small public interface per module. Other modules call that interface, not the module's internals.
- Do not share tables across modules. Each module owns its tables. If another module needs data, it asks through the interface or receives an event. This mirrors the "data private to the service" rule and is what makes a later extraction possible.
- Prefer asynchronous events between modules where the business allows. In-process events now can become messages on a broker later. Microsoft's microservices guidance names messaging platforms such as Apache Kafka and Azure Service Bus for that role. For a comparison of brokers, see Kafka vs RabbitMQ vs AWS SQS.
- Enforce the rules automatically. Use your language's module or package visibility, plus an architecture test or dependency checker in CI, so boundaries do not erode in code review.
- Align ownership. Microsoft's Conway's law tip says to define boundaries through domain analysis and then align team ownership to them. If one team must own many unrelated contexts, or one context needs many teams, revisit the boundaries or the team structure.
A sketch of a layout:
src/
billing/
api/ public interface other modules may call
domain/ entities, rules (private to the module)
data/ tables and queries owned by billing
fulfillment/
api/
domain/
data/
shared-kernel/ small, stable types only (IDs, money, time)Keep the shared kernel tiny. Microsoft lists sharing common libraries between microservices as an antipattern because it creates tight coupling, and a large shared kernel recreates the same coupling inside your monolith.
Signals that it is time to split
Treat any of these as a prompt to evaluate a split, not an automatic trigger. Each maps to a benefit from the Microsoft page.
| Signal | Related benefit |
|---|---|
| One module needs far more compute or memory than the rest | Independent scaling |
| A fix in one area is blocked by unfinished work in another | Independent deployment and agility |
| Two teams constantly edit the same files | Small, focused teams |
| A component needs a different runtime or library | Mix of technologies |
| A non-critical feature's failures take down critical ones | Fault isolation |
| Schema changes in one area keep breaking other areas | Data isolation |
Also confirm that the module boundary has been stable for a while. Microsoft notes that refactoring across service boundaries is difficult, so splitting a boundary that still moves a lot is expensive.
How to extract a service when you need one
Microsoft's Strangler Fig pattern gives a controlled, incremental path. The pattern is described for legacy migration, but the mechanics suit extracting a module from a monolith.
- Introduce a facade. Put a proxy between clients, the existing system and the new service. At first it routes nearly everything to the existing system.
- Move one capability. Implement it as a new service and shift the facade's routing for that capability.
- Handle shared data. Microsoft's database example introduces an isolated domain database, loads history with an ETL process, and uses change data capture to keep it in sync. You validate consistency before cutover, and you can roll back to the monolithic database until you remove the old tables.
- Plan for cross-system calls. During the migration the new service may need unmigrated functionality and the reverse. Microsoft suggests the Anti-corruption Layer pattern to translate between them.
- Remove the old code last. Treat deleting the legacy objects as a deliberate final step after validation.
Watch the facade. Microsoft warns it must keep pace with the migration and must not become a single point of failure or performance bottleneck. The pattern may be unsuitable if requests cannot be intercepted or if you are migrating a small system where replacing the whole thing is simple.
Operating your first extracted service
Once the first service exists, the Microsoft best practices become requirements:
- Automate build, test and deployment with CI/CD, and deploy services independently.
- Use centralized logging, distributed tracing (OpenTelemetry) and metrics.
- Put an API gateway in front of clients instead of exposing services directly.
- Keep configuration outside the service and security logic out of business code.
- Isolate failures with resiliency patterns so one slow dependency does not cascade.
- Limit the number of languages and frameworks, and standardize logging, monitoring and deployment.
If this list feels heavy for your team, that is useful information about whether to proceed.
Troubleshooting common problems
| Problem | Likely cause | What to do |
|---|---|---|
| Services call each other constantly | Boundaries cut through one capability | Microsoft says chatty communication can signal tight coupling and low cohesion; merge or redraw |
| Every change touches several services | Functions that change together live apart | Package and deploy things that change together |
| A request chain is slow | Long synchronous dependency chains | Reduce hops, avoid chatty APIs, consider asynchronous messaging |
| Data is inconsistent across services | Multi-service change treated like one transaction | Design for eventual consistency and compensating actions |
| Nobody can trace a failure | No correlated logging | Add distributed tracing and shared correlation IDs |
| A module in the monolith reads another's tables | Boundary rule not enforced | Add a CI dependency check and route access through the module interface |
Summary checklist
- You can state the specific problem a split would solve in one sentence
- A cheaper fix (queue, more instances, feature flags) was considered first
- CI/CD, centralized logging and tracing are in place or planned
- The team can accept eventual consistency where services share a process
- Module boundaries follow business capabilities and have been stable
- Each module owns its data and exposes a small interface
- Any extraction uses a facade and moves one capability at a time
- Team ownership lines up with the boundaries
Comments
Questions and tips are welcome. Comments are checked by the EpiFive team before they appear; please do not share passwords, phone numbers or other private details.