Designing a new feature in a microservice application requires careful and well-reasoned scoping of microservices. You need to decide when to build new services or extend existing services, where boundaries lie between those services, and how those services should collaborate.
Well-designed services have three key characteristics: they’re responsible for a single capability, independently deployable, and replaceable. If your microservices have the wrong boundaries, or are too small, they can become tightly coupled, making them challenging to deploy independently or replace. Tight coupling increases the impact, and therefore the risk, of change. If your services are too large — taking on too much responsibility — they become less cohesive, increasing friction in ongoing development.
Even if you get it right the first time, you need to keep in mind that the requirements and needs of most complex software applications will evolve over time, and approaches that worked early in that application’s lifetime may not always remain suitable. No design is perfect forever.
You’ll face additional challenges in longer running applications (and larger engineering organizations). Your services may rely on a web of dependencies managed by multiple teams — as an engineer in one team, you’ll need to design cohesive functionality while relying on services that won’t necessarily be under your control. And you’ll need to know when to retire and migrate away from services that no longer meet the needs of the wider system.
In this chapter, we’ll walk you through designing a new feature using microservices. We’ll use that example to explore techniques and practices that you can use to guide the design of maintainable microservices in both new and longer running microservice applications.
Remember SimpleBank? The team is doing well — customers love their product! But SimpleBank has discovered that most of those customers don’t want to pick their own investments — they’d much rather have SimpleBank do the hard work for them. Let’s take this problem and work out how to solve it with a microservice application. In the next few sections, we’ll develop the design in four stages:
This will build on the small collection of services we explored in chapters 2 and 3: orders, market gateway, account transactions, fees, market data, and holdings.
First, let’s understand the business problem you’re trying to solve. In the real world, you could carry out the discovery and analysis of business problems using several techniques, such as market research, customer interviews, or impact mapping. As well as understanding the problem, you’d need to decide whether it was one your company should solve. Luckily, this isn’t a book about product management — you can skip that part.
Ultimately, SimpleBank’s customers want to invest money, either up front or on a regular basis, and see their wealth increase, either over a defined period or to meet a specific goal, such as a deposit on a house. Currently, SimpleBank’s customers need to choose how their money is invested — even if they don’t have a clue about investing. An uninformed investor might choose an asset based on high predicted returns, without realizing that higher returns typically mean significantly higher risk.
To solve this problem, SimpleBank could make investment decisions on the customer’s behalf by allowing the customer to choose a premade investment strategy. An investment strategy consists of proportions of different asset types — bonds, shares, funds, and so on — designed for a certain level of risk and investment timeline. When a customer adds money to their account, SimpleBank will automatically invest that money in line with this strategy. This setup is summarized in figure 4.1.
Based on figure 4.1, you can start to identify the use cases you need to satisfy to solve this problem:
Over the next few sections, we’ll explore these use cases. When identifying use cases in your own domain, you may prefer to use a more structured and exhaustive approach, such as behavior-driven development (BDD) scenarios. What’s important is that you start to establish a concrete understanding of the problem, which you then can use to validate an acceptable solution.
After you’ve identified your business requirements, your next step is to identify the technical solution: which features you need to build and how you’ll support them with existing and new microservices. Choosing the right scope and purpose for each microservice is essential to building a successful and maintainable microservice application.
This process is called service scoping. It’s also known as decomposition or partitioning. Breaking apart an application into services is challenging — as much art as science. In the following sections, we’ll explore three strategies for scoping services:
You don't necessarily use these approaches in isolation; in many microservice applications, you’ll combine scoping strategies to design services appropriate to different scenarios and requirements.
A business capability is something that an organization does to generate value and meet business goals. Microservices that are scoped to business capabilities directly reflect business goals. In commercial software development, these goals are usually the primary drivers of change within a system; therefore, it’s natural to structure the system to encapsulate those areas of change. You’ve seen several business capabilities implemented in services so far: order management, transaction ledgers, charging fees, and placing orders to market (figure 4.2).
Business capabilities are closely related to a domain-driven design approach. Domain-driven design (DDD) was popularized by Eric Evans’ book of the same name and focuses on building systems that reflect a shared, evolving view, or model, of a real-world domain.1 One of the most useful concepts that Evans introduced was the notion of a bounded context. Any given solution within a domain might consist of multiple bounded contexts; the models inside each context are highly cohesive and have the same view of the real world. Each context has a strong and explicit boundary between it and other contexts.
Bounded contexts are cohesive units with a clear scope and an explicit external boundary. This makes them a natural starting point for scoping services. Each context demarcates the boundaries between different areas of your solution. This often has a close correspondence with organizational boundaries; for example, an e-commerce company will have different needs — and different teams — for shipping versus customer payments.
To begin with, a context typically maps directly to a service and an area of business capability. As the business grows and becomes more complex, you may end up breaking a context down into multiple subcapabilities, many of which you’ll implement as independent, collaborating services. From the perspective of a client, though, the context may still appear as a single logical service.
You can design services to support creating investment strategies using a business capability approach. You might want to get a sketch pad to work through this one. To help you work through this example and give the use case more shape, we’ve wireframed what the UI for this feature might look like in figure 4.3.
To design services by business capability, it’s best to start with a domain model: some description of the functions your business performs in your bounded context(s) and the entities that are involved. From figure 4.3, you’ve probably identified these already. A simple investment strategy has two components: a name and a set of assets, each with a percentage allocation. An administrative staff member at SimpleBank will create a strategy. We’ve drafted those entities in figure 4.4.
The design of these entities helps you understand the data your services own and persist. Only three entities, and it already looks like you’ve identified (at least) two new services: user management and asset information. The user and asset entities are both part of distinct bounded contexts:
Interestingly, these different domains reflect the organization of SimpleBank itself. A dedicated operational team manages asset data; likewise, user management. This comparability is desirable, as it means your services will reflect real-world lines of cross-team communication.
More on that later — let’s get back to investment strategies. You know that you can associate them with customer accounts and use them to generate orders. Accounts and orders are both distinct bounded contexts, but investment strategies don’t belong in either one. When strategies change, the change is unlikely to affect accounts or orders themselves. Conversely, adding investment strategies to either of those existing services will hamper their replaceability, making them less cohesive and less amenable to change.
These factors indicate that investment strategies are a distinct business capability, requiring a new service. Figure 4.5 illustrates the relationships between this context and your existing capabilities.
You can see that some contexts are aware of information that belongs to other contexts. Some entities within your context are shared: they’re conceptually the same but carry unique associations or behavior within different contexts. For example, you use assets in multiple ways:
The model we’ve drawn out in figure 4.5 doesn’t tell you much about the behavior of a service; it only tells you the business scope your services cover. Now that you have a firmer idea of where your service boundaries lie, you can draft out the contract that your service offers to other services or end users.
First, your investment strategies service needs to expose methods for creating and retrieving investment strategies. Other services or your UI can then access this data. Let’s draft out an endpoint that allows creating an investment strategy. The example shown in listing 4.1 uses the OpenAPI specification (formerly known as Swagger), which is a popular technique for designing and documenting REST API interfaces. If you’re interested in learning more, the Github page for the OpenAPI specification2 is a good place to start.
Listing 4.1 API for the investment strategies service
openapi: "3.0.0"
info: ①
title: Investment Strategies ①
servers: ①
- url: https://investment-strategies.simplebank.internal ①
paths:
/strategies:
post: ②
summary: Create an investment strategy
operationId: createInvestmentStrategy
requestBody: ③
description: New strategy to create
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewInvestmentStrategy' ④
responses:
'201':
description: Created strategy
content:
application/json:
schema:
$ref: '#/components/schemas/InvestmentStrategy' ⑤
components: ⑥
schemas:
NewInvestmentStrategy: ⑦
required:
- name
- assets
properties:
name:
type: string
assets: ⑧
type: array ⑧
items: ⑧
$ref: '#/components/schemas/AssetAllocation' ⑧
AssetAllocation:
required:
- assetId
- percentage
properties:
assetId:
type: string
percentage:
type: number
format: float
InvestmentStrategy:
allOf:
- $ref: '#/components/schemas/NewInvestmentStrategy' ⑨
- required:
- id
- createdByUserId
- createdAt
properties:
id:
type: integer
format: int64
createdByUserId:
type: integer
format: int64
createdAt:
type: string
format: date-time
If you’re going to use strategies again later — and you are — you’ll need to retrieve them. Immediately under your paths:
element in listing 4.1, add the code in the following listing.
Listing 4.2 API for retrieving strategies from the investment strategies service
/strategies/{id}: ①
get:
description: Returns an investment strategy by ID
operationId: findInvestmentStrategy
parameters: ②
- name: id ②
in: path ②
description: ID of strategy to fetch ②
required: true ②
schema: ②
type: integer ②
format: int64 ②
responses:
'200':
description: investment strategy
content:
application/json:
schema:
$ref: '#/components/schemas/InvestmentStrategy' ③
You also should consider what events this service should emit. An event-based model aids in decoupling services from each other, ensuring that you can choreograph long-term interactions, rather than explicitly orchestrate them.
For example, imagine that creating a strategy will trigger email notifications to potentially interested customers. This is separate from the scope of the investment strategies service itself; it has no knowledge of customers (or their preferences). This is an ideal use case for events. If a POST
to /strategies
post-hoc triggers an event — let’s call it StrategyCreated
— then arbitrary microservices can listen for that event and act appropriately. Figure 4.6 illustrates the full scope of your service’s API.
Great work — you’ve identified all the capabilities that you require to support this use case. To see how this fits together, you can map the investment strategies service and the other capabilities you’ve identified to the wireframe (figure 4.7).
Let’s summarize what you’ve done so far:
This approach results in services that are relatively stable, cohesive, oriented to business value, and loosely coupled.
Each bounded context provides an API to other contexts, while encapsulating internal operation. Let’s take asset information as an example (figure 4.8):
The private/public divide provides a useful mechanism for service evolution. Early in a system’s lifecycle, you might choose to build coarser services, representing a high-level boundary. Over time, you might decompose services further, exposing behavior from nested contexts. Doing this maintains replaceability and high cohesion, even as business logic increases in complexity.
In the previous sections, you identified the natural seams within the organization’s business domain and applied them to partition your services. This approach is effective because it maps services to the functional structure of a business — directly reflecting the domain in which an organization operates. But it’s not perfect.
Partitioning by business capabilities requires having significant understanding of the business or problem domain. This can be difficult. If you don’t have enough information — or you’ve made the wrong assumptions — you can’t be completely certain you’re making the right design decisions. Understanding the needs of any business problem is a complex, time-consuming and iterative process.
This problem isn’t unique to microservices, but misunderstanding the business scope — and reflecting it incorrectly in your services — can incur higher refactoring costs in this architecture, because both data and behavior can require time-consuming migration between services.
Similarly, a business capability approach is biased toward the initial development of coarse-grained services that cover a large business boundary — for example, orders, accounts, or assets. New requirements increase the breadth and depth of that area, increasing the scope of the service’s responsibility. These new reasons to change can violate the single responsibility principle. It’ll be necessary to partition that service further to maintain an acceptable level of cohesion and replaceability.
So far, your services have been nouns, oriented around objects and things that exist within the business domain. An alternative approach to scoping is to identify verbs, or use cases within your application, and build services to match those responsibilities. For example, an e-commerce site might implement a complex sign-up flow as a microservice that interacts with other services, such as user profile, welcome notifications, and special offers.
This approach can be useful when
Let’s apply this approach to SimpleBank to understand how it differs from noun-oriented decomposition. Get your pencil and paper ready!
A customer can invest money into an investment strategy. This will generate appropriate orders; for example, if the customer invests $1,000, and the strategy specifies 20% should be invested in Stock ABC, an order will be generated to purchase $200 of ABC.
This raises several questions:
You could build this capability into your existing investment strategies service. But placing orders might unnecessarily widen the scope of responsibility that the service encapsulates. Likewise, the capability doesn’t make sense to add to the orders service. Coupling all possible sources of orders to that service would give it too many reasons to change.
You can draft out an independent service for this use case as a starting point — call it PlaceStrategyOrders. Figure 4.9 sketches out how you’d expect this service to behave.
Consider the input to this service. For orders to be placed, this service needs three things: the account placing them, the strategy to use, and the amount to invest. You can formalize that input, as shown in the following listing.
Listing 4.3 Draft input for PlaceStrategyOrders
paths:
/strategies/{id}/orders: ①
post:
summary: Place strategy orders
operationId: PlaceStrategyOrders
parameters:
- name: id
in: path
description: ID of strategy to order against
required: true
schema:
type: integer
format: int64
requestBody:
description: Details of order
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/StrategyOrder'
components:
schemas:
StrategyOrder:
required:
- destinationAccountId ②
- amount ②
properties:
destinationAccountId:
type: integer
format: int64
amount:
type: number
format: decimal
This is elegant but a little too simple. If you assume your payment is coming from an external source, you can’t execute orders until those funds are available. It doesn’t make sense for PlaceStrategyOrders to handle receipt of funds — this is clearly a distinct business capability. Instead, you can link placing strategy orders to a payment, as follows.
Listing 4.4 Using payment ID for PlaceStrategyOrders
components:
schemas:
StrategyOrder:
required:
- destinationAccountId
- amount
- paymentId ①
properties:
destinationAccountId:
type: integer
format: int64
amount:
type: number
format: decimal
paymentId:
type: integer
format: int64
This anticipates the existence of a new service capability: payments. This capability should support
Because you know that payments aren’t instantaneous, you’d expect this service to trigger asynchronous events that other services can listen for, such as PaymentCompleted. Figure 4.10 illustrates this payments capability.
From the perspective of PlaceStrategyOrders, it doesn’t matter how you implement the payments capability, as long as something implements the interface the consumer expects. It might be a single service — Payments — or a collection of action-oriented services, for example, CompleteBankTransfer.
You can summarize what you’ve designed so far in a sequence diagram (figure 4.11).
There’s one missing element in this diagram: getting these orders to market. As mentioned, although this service generates orders, this capability clearly doesn’t belong within your existing orders service. The orders service exposes behavior that multiple consumers can use, including this new service (figure 4.12); although the source of orders differs, the process of placing them remains the same.
Lastly, you need to persist the link between these orders and the strategy and investment that created them. PlaceStrategyOrders should be responsible for storing any request it receives — it clearly owns this data. Therefore, you should record any order IDs within the strategy order service to preserve that foreign key relationship. You could also record the order source ID — the ID of this investment strategy investment request — within the orders service itself, although it seems less likely you’d query data in that direction.
The orders service emits OrderCompleted
events when an order has been completed. Your strategy orders service can listen for these events to reflect that status against the overall investment request.
You can add the orders service and tie this all together as shown in figure 4.13.
Great! You’ve designed another new service. Unlike the previous section, you designed a service that closely represented a specific complex use case, rather than a broad capability.
This resulted in a service that was responsible for a single capability, replaceable, and independently deployable, meeting your desired characteristics for well-scoped microservices. In contrast, unlike if you’d focused on business capabilities, the tight focus of this service on a single use case limits potential for reuse in other use cases in the future. This inflexibility suggests that fine-grained use case services are best used in tandem with coarser grained services, rather than alone.
We’ve identified an interesting pattern in the above examples: multiple higher level microservices access a coarse-grained underlying business capability. This is especially prevalent in a verb-oriented approach, as the data needs of different actions often overlap.
For example, imagine you have two actions: update order and cancel order. Both operations operate against the same underlying order state, so neither can exclusively own that state itself, and you need to reconcile that conflict somewhere. In the previous examples, the orders service took care of the problem. This service is the ultimate owner of that subset of your application’s persistent state.
This pattern is similar3 to Bob Martin’s clean architecture4 or Alistair Cockburn’s hexagonal architecture. In those models, the core of an application consists of two layers:
Around those layers, you use interface adapters to connect these business-logic concerns to application-level implementation concerns, such as particular web frameworks or database libraries. Similarly, at an intraservice level, your use cases (or actions) interact with underlying entities (or stores) to generate some useful outcome. You then wrap them in a façade, such as an API gateway, to map from your underlying service-to-service representations to an output friendly to an external consumer (for example, a RESTful API). Figure 4.14 sketches out that arrangement.
This architecture is conceptually elegant, but you need to apply it judiciously in a microservice system. Treating underlying capabilities as, first and foremost, stores of persistent state can lead to anemic, “dumb” services. These services fail to be truly autonomous because they can’t take any action without being mediated by another, higher level service. This architecture also increases the number of remote calls and the length of the service chain you need to perform any useful action.
This approach also risks tight coupling between actions and underlying stores, hampering your ability to deploy services independently. To avoid these pitfalls, we recommend you design microservices from the inside out, building useful coarse-grained capabilities before building fine-grained action-oriented services.
In chapter 2, we discussed the difference between orchestration and choreography in service interaction. A bias toward choreography tends to result in more flexible, autonomous, and maintainable services. Figure 4.15 illustrates the difference between these approaches.
If you scope services by use case, you might find yourself writing services that explicitly orchestrate the behavior of several other services. This isn’t always ideal:
When designing services that reflect use cases, it’s important to consider their place within a broader chain of responsibilities. For example, the PlaceStrategyOrders service you designed earlier both orchestrates behavior (placing orders) and reacts to other events (payment processing). Taking a balanced approach to choosing orchestration or choreography reduces the risk of building services that lack autonomy.
In an ideal world, you could build any feature by combining existing microservices. This might sound impractical, but it’s interesting to consider how you maximize the reusability — and therefore long-term utility — of the services you build.
So far, we’ve taken a predominantly functional approach to decomposing services. This approach is effective but has limitations. Functional decomposition is biased toward the present needs of an application and doesn’t explicitly consider how that application might evolve. A purely functional approach can constrain the future growth of a system by resulting in services that are inflexible in the face of new or evolving requirements, thereby increasing the risk of change.
Therefore, as well as considering the functionality of your system, you should consider where that application is likely to change in the future. This is known as volatility. By encapsulating areas that are likely to change, you help to ensure that uncertainty in one area doesn’t negatively impact other areas of the application. You can find an analogy to this in the stable dependencies principle in object-oriented programming: “a package should only depend on packages that are more stable than it is.”
SimpleBank’s business domain has multiple axes of volatility. For example, placing an order to market is volatile: different orders need to go to different markets; SimpleBank might have different APIs to each market (for example, through a broker, direct to an exchange); and those markets might change as SimpleBank broadens its offering of financial assets.
Tightly coupling market interaction as part of the orders service would lead to a high degree of instability. Instead, you’d split the market service and ultimately build multiple services to meet the needs of each market. Figure 4.16 illustrates this approach.
Let’s take one more example: imagine you have more than one type of investment strategy. Perhaps you have strategies that are optimized by deep learning: the performance of assets on the market should drive adjustments to future strategy allocations.
Adding this complex behavior to your InvestmentStrategies service would significantly broaden its reasons to change — reducing cohesiveness. Instead, you should add new services with responsibility for that behavior — as you can see in figure 4.17. By doing this, you can develop and release these services independently without unnecessary coupling between different features or rates of change.
Ultimately, good architecture strikes a balance between the current and future needs of an application. If microservices are too narrowly scoped, you might find the cost of change becomes higher in the future as you become increasingly constrained by earlier assumptions about the limits of your system. On the flipside, you should always be careful to keep YAGNI — “you aren’t gonna need it” — in mind. You may not always have the luxury of time (or money) to anticipate and meet every possible future permutation of your application.
The services you’ve designed so far have reflected actions or entities that map closely to your business capabilities, such as placing orders. These business-oriented services are the primary type you’ll build in any microservice application.
You can also design services that reflect technical capabilities. A technical capability indirectly contributes to a business outcome by supporting other microservices. Common examples of technical capabilities include integration with third-party systems and cross-cutting technical concerns, such as sending notifications.
Let’s work through an example. Imagine that SimpleBank would like to notify a customer — perhaps through email — whenever a payment has been completed. Your first instinct might be to build that code within your payments service (or services). But that approach has three problems:
The first and second points suggest that this should be a separate service; the third point suggests you might need multiple services — one to handle each type of notification. Figure 4.18 sketches that out. Your notification services can listen to the PaymentCompleted
event that your payments service emits.
You can configure your group of notification services to listen to any events — from any service — that should result in a notification. Each service will need to be aware of a customer’s contact preferences and details to send notifications. You could store that information in a separate service, such as a customers service, or have each service own it. This area has hidden dimensions of complexity; for example, many customers might own the payment’s destination account, triggering multiple notifications.
You may have realized that the notification services are also responsible for generating appropriate message content based on each event, which suggests they could grow significantly in the future, in line with the potential number of notifications. It eventually might be necessary to split message content from message delivery to reduce this complexity.
This example illustrates that implementing technical capabilities maximizes reusability while simplifying your business services, decoupling them from nontrivial technical concerns.
You should use a technical capability to support and simplify other microservices, limiting the size and complexity of your business capabilities. Partitioning these capabilities is desirable when
Encapsulating these capabilities in separate services captures axes of volatility — areas that are likely to change independently — and maximizes service reusability.
In certain scenarios, it’s unwise to partition a technical capability. In some situations, extracting a capability will reduce the cohesiveness of a service. For example, in classic SOA, systems were often decomposed horizontally, in the belief that splitting data storage from business functionality would maximize reusability. Figure 4.19 illustrates how requests would be serviced in this approach.
Unfortunately, the intended reusability came at a high cost. Splitting those layers of an application led to tight coupling between different deployable units, as delivering individual features required simultaneous change across multiple applications (figure 4.20). When you have to coordinate changes to distinct components, this leads to error-prone, lock-step deployments — a distributed monolith.
If you focus on business capabilities first, you’ll avoid these pitfalls. But you should carefully scope any technical capability to ensure it’s truly autonomous and independent from other services.
Scoping microservices is as much art as science. A large part of software design is finding effective ways to achieve the best solution when faced with ambiguity:
Suboptimal service partitioning in microservices can be costly: it adds friction to development and extra effort to refactoring.
In this section, we’ll explore a few approaches you can use to make practical service decisions when the right solution isn’t obvious. To start, we’ve talked a lot about the importance of keeping the responsibility of a service focused, cohesive, and limited, so what I’m about to say might sound a little counterintuitive. Sometimes, when in doubt about service boundaries, it’s better to build larger services.
If you err on the side of building services that are too small, it can lead to tight coupling between different services that should be combined in one service. This indicates you’ve decomposed a business capability too far, making responsibility unclear and making it more difficult — and costly — to refactor this element of functionality.
If instead you combine that functionality into a larger service, you reduce the cost of future refactoring, as well as avoiding intractable cross-service dependencies. Likewise, one of the most expensive costs you’ll incur in a microservice application is changing a public interface; reducing the breadth of interfaces between components aids in maintaining flexibility, especially in early stages of development.
Understand that making a service larger also incurs a cost, because larger services become more resistant to change and difficult to replace. But at the beginning of its life, a service will be small. The costs associated with a service being too large are less than the costs of complexity that decomposing too far introduces. You need to carefully observe both service size and complexity to ensure you’re not building more monoliths.
Here, it’s useful to apply a key principle of lean software development: decide as late as possible. Because building a service incurs cost in both implementation and operation, avoiding premature decomposition when faced with uncertainty can give you time to develop your understanding of the problem space. It also will ensure you’re making well-informed decisions about the shape of the application as it grows.
The modeling and scoping techniques from earlier in this chapter will help you identify when a service has become too large. Often, you’ll be able to identify possible seams quite early in the lifetime of a service. If so, you should endeavor to design your service internals to reflect them, whether through class and namespace design or as a separate library.
Maintaining disciplined internal module boundaries, with a clear public API, is generally sound software design. In a microservice, it reduces the cost of future refactoring by reducing the chance that code becomes highly coupled and difficult to untangle. That said, be careful — an API that’s well-designed in the context of a code library may not always be ideal as the interface to a microservice.
We’ve talked about planning for future decomposition, but we also should talk about service retirement. Microservice development requires a certain degree of ruthlessness. It’s important to remember that it’s what your application does that matters, not the code. Over time — and especially if you start with larger services — you’ll find it necessary to either carve out new microservices from existing services or retire microservices altogether.
This process can be difficult. Most importantly, you need to ensure that consuming services don’t get broken and that they migrate in a timely way to any replacement service.
To carve out new services, you should apply the expand-migrate-contract pattern. Imagine you’re carving out a new service from your orders service. When you first built the orders service, you were confident that it’d fit the needs of all order types, so you built it as a single service. But one order type has turned out to be different from the others, and supporting it has bloated your original service.
First, you need to expand — pulling the target functionality into a new service (figure 4.21). Next, you need to migrate consumers of your old service to the new service (figure 4.22). If access is through an API gateway, you can redirect appropriate requests to your new service.
But if other services call the orders service, you need to migrate those usages. Telling other teams to migrate doesn’t always work (competing priorities, release cycles, and risk). Instead, you need to either make sure your new service is compelling — make people want to invest effort in migration — or do that migration for them.
To complete the process, you have one last step. Finally, you can contract the original service, removing the now obsolete code (figure 4.23).
Great, you made it! This measured, multistep process systematically retires or migrates functionality while reducing the risk of breaking existing service consumers.
The examples so far have mostly assumed that a single team is responsible for building and changing microservices. In a large organization, different teams will own different microservices. This isn’t a bad thing — it’s an important part of scaling as an engineering team.
As we pointed out earlier, bounded contexts themselves are an effective way of splitting application ownership across different teams in an organization. Forming teams that own services in specific bounded contexts takes advantage of the inverse version of Conway’s Law: if systems reflect the organizational structure that produces them, you can attain a desirable system architecture by first shaping the structure and responsibilities of your organization. Figure 4.24 illustrates how SimpleBank might organize its engineering teams around the services and bounded contexts you’ve identified so far.
Splitting ownership and delivery of services across teams has three implications:
These implications can present an immense challenge, but applying a few tactics can help:
These sorts of tactics touch on the people side of microservices. This is a substantial topic by itself, which we’ll explore in depth in the final chapter of this book.