Chapter 1. Brave new world

Software development is an art. It isn’t predictable enough to be engineering. It isn’t rigorous enough to be science. We’re artists—and that’s not a good thing. We find it hard to work in teams, we find it hard to deliver on deadlines, and we find it hard to focus on practical results. Anyone can claim to be a software developer; opening a text editor is no more difficult than picking up a paint brush. A small number of painters are 10 times better than others. A small number of coders also are 10 times better than others. But most of us are working on bad instincts.

Unlike art, software has to work. It has business problems to solve, users to serve, and content to deliver. We have a thousand opinions among ourselves about how to do things correctly, yet many software projects are delivered late and over budget. Much software, especially that produced by large companies, offers users a terrible experience. We have a self-important belief in our work, as artists, that isn’t connected to the reality of our systematic failure. We need to admit we have a problem, understand the nature of that problem, and use science and engineering to solve the problem.

1.1. The technical debt crisis

The problem is that we can’t write software fast enough. We can’t write software that meets business needs and that is sufficiently accurate and reliable, within the time constraints set by the markets in which our companies operate. When requirements change in the middle of a project, it damages our architecture so badly that we spiral into a death march of hacks and kludges to force our data structures, concepts, and entity relationships into compliance. We try to refactor or rewrite, and that delays things even further.

You might be tempted to blame the business itself for this state of affairs. Requirements are always underspecified, and they keep changing. Deadlines are imposed without regard to the complexity of the problem. Bad management wastes time and eats up development schedules. It’s easy to become cynical and blame these issues on the stupidity of others.

Such cynicism is self-defeating and naïve. The world of business is harsh, and markets are unforgiving. Our nontechnical colleagues have complex challenges of their own. It’s time to grow up and accept that we have a problem: we can’t write software fast enough.

But why?

Be careful. If there were a silver bullet, we’d already be using it. Take methodologies: we fight over them because there are no clear winners. Some methodologies certainly are better than others, in the same way that a sword is a better weapon than a dagger, but neither is of much use in the gun fight that is enterprise software development. Or take a best practice like unit testing, which feels like it’s valuable. Just because something feels good doesn’t mean it is good. Intuitions can be misleading. We have a natural tendency toward superstition, forcing development practices into Procrustean beds.[1] Few of our best practices have any measure of scientific validation.

1

Procrustes was a son of the Greek god Poseidon. He took great pleasure in fitting his house guests into an iron bed. To make the guest fit, he would either amputate their feet or stretch them on a rack. A Procrustean bed is a behavior that doesn’t serve its intended purpose; it only perpetuates a superstition. Insistence on high levels of unit-test coverage is the prime example in our industry. The coverage target and the effort to achieve it are seldom adjusted to match the value generated by the code in production.

The problem is that we don’t know how to pay down technical debt. No matter how beautifully we crystallize our initial designs, they fracture against the changing nature of reality. We try to make our software perfect: perfectly correct and able to perfectly meet requirements. We have all sorts of perfection-seeking behavior, from coding standards, to type systems, to strict languages, to hard integration boundaries, to canonical data models. And yet we still end up with a legacy mess to support.

We’re not stupid. We know that we have to cope with change. We use flexible data structures, with scope for growth (in the right places, if we’re lucky). We have this thing called refactoring, which is the technical term for getting caught making bad guesses. At least we have a professional-sounding term for the time we waste rewriting code so that we can start moving forward again.

We have components, which are the machine guns of software architecture: force multipliers that let you build big things out of small things. You only have to solve each problem once. At heart, object-oriented languages are trying to be component systems, and so are web services.[2] So was structured programming (a fancy name for dropping the GOTO statement). We have all these technical best practices, and we’re still too slow. Components, in particular, should make us faster. Why don’t they?

2

We even have fancy component systems that were designed from the ground up, like OSGi and CORBA. They haven’t delivered composability. The Node.js module system is a relatively strong approach and makes good use of semantic versioning, but it’s restricted to one platform and exposes all the nuts and bolts of the Java-Script language. UNIX pipes are about as good as it gets, if you’re looking for something that’s widely used.

We haven’t been thinking about components in the right way for a long time in mainstream enterprise programming.[3] We can just about build library components to talk to databases, perform HTTP requests, and package up sorting algorithms. But these components are technical infrastructure. We’re not so good at writing reusable components that deliver business logic. Components like that would speed up development. We’ve focused on making our components so comprehensive in functionality that it’s difficult to compose them together; we have to write lots of glue code. By trying to cover too many cases, we make the simple ones too complex.

3

This isn’t universally true. The functional language communities in particular treat composability as a first-class citizen. But consider that, whereas you can compose pretty much anything on the UNIX command line using pipes, functions aren’t generically composable without extra work to make their inputs and outputs play nicely together.

What is business logic?

In this book, business logic is the part of the functionality that’s directly specific to the business goal at hand. User-profile management is business logic. A caching layer isn’t.

Business logic is your representation of the processes of your business using the structures of your programming language. What is business logic in one system many not be in another. It can change within the same system over time. The term is suggestive, not prescriptive.

The thing that makes components work is composition: making big things out of small things. The component models that reduce your workload, such as UNIX pipes and functional programming, have this feature. You combine parts to create something new.

Composition is powerful. It works because it only does one thing: adds components together. You don’t modify components; instead, you write new ones to handle special cases. You can code faster because you never have to modify old code. That’s the promise of components.

Consider the state of the software nation. The problem is that we can’t code fast enough. We have this problem because we can’t cope with technical debt. We don’t work with an engineering mindset. We haven’t used scientific methods to validate our beliefs. As a solution, components should be working, but they aren’t. We need to go back to basics and create a component model that delivers practical composition. Microservices,[4] built the right way, can help do that.

4

If you’re looking for a definition of the term microservice, you’re not going to find it in this book. We’re discussing an approach to software architecture that has its own benefits and trade-offs. Substance counts more than sound bites.

1.2. Case study: A microblogging startup

We’ll use case studies throughout this book to illustrate practical applications of the microservice approach. We’ll use this first one to introduce some of the core principles of the architecture before we analyze them more deeply. The case studies will keep you connected to the practical side of the discussion and allow you to make a critical assessment of the ideas presented—would you build things the same way?

In this case study, you’re building the minimum viable product (MVP)[5] for a new startup. The startup has come up with a crazy new idea called microblogging. Each blog entry can be only a few paragraphs long, with a maximum of 1,729 characters. This somewhat arbitrary limit was chosen by the founders as the most uninteresting number they could think of. The startup is called ramanujan.io.[6] It may seem strange to use a startup as a case study when our primary concern is enterprise software development. But isn’t the goal to be as nimble as a startup?

5

The MVP is a product-development strategy developed by Eric Ries, a founder of the IMVU chat service (founded 2004): build only the minimum set of features that lets you validate your assumptions about a market, and then iterate on those features and assumptions until you find a product that fits the market.

6

You can find the full source code and a workshop at www.manning.com/books/the-tao-of-microservices and http://ramanujan.io.

We’ll follow the startup through a series of iterations as it gets the MVP up and running. It’s sometimes said that microservices create too much overhead at the start of a project. That misses the primary benefit of microservices—the ability to add functionality quickly!

1.2.1. Iteration 0: Posting entries

This is the first iteration. We’ll use iterations to follow the story of the startup.

A microblogging system lets users post entries: short pieces of text. There’s a page where users can view their own entries. This set of activities seems like a good place to start.

What activities happen in the system?

  • Posting an entry
  • Listing previous entries

There are all sorts of other things, like having a user account and logging in, that we’ll ignore for the sake of keeping the case study focused. These activities are amenable to the same style of analysis.

The activities can be represented by messages. No need to overthink the structure of these messages, because you can always change the mapping from message to microservice later. To post an entry, let’s have a message that tells you which user is posting the entry, and the text of the entry. You’ll also need to classify the message in some way so that you know what sort of message it is. The property-value pair post:entry does this job—a little namespacing is always a good idea. Let’s use JSON as the data format.

Listing 1.1. Posting an entry
{
  post: 'entry',
  user: 'alice',
  text: 'Curiouser and curiouser!'
}

Any interested microservices can recognize this message by looking for the pattern post:entry in the message’s top-level properties. For now, you’ll assume that messages can make their way to the right microservice without worrying too much about how that happens. (Chapter 2 has much more to say about message routing.)

You also need a message for listing entries. Standing back for a moment, you can assume that the system may include other kinds of data down the road. There are certainly common operations that you’ll perform on data entities, such as loading, saving, and listing. Let’s add a store property to the message to create a namespace for messages concerned with persistent data. In this case, you want to list things from the data store, so the property-value pair store:list seems natural. You’ll use kind:entry to identify the data entity as an entry, assuming you’ll have other kinds of data entities later.

Listing 1.2. Listing entries for the user alice
{
  store: 'list',
  kind: 'entry',
  user: 'alice'
}

Time to put on your architecture hat. There’s a family of data-operation messages here, with a corresponding set of patterns:

  • store:list,kind:entry—Lists entries, perhaps with a query constraint on the result list
  • store:load,kind:entry—Loads a single entry, perhaps using an id property in the message
  • store:save,kind:entry—Saves an entry, creating a new database row if necessary
  • store:remove,kind:entry—Removes an entry from the database, using the id property to select it

This is an initial outline of the possible data-operation message patterns. This set of properties feels workable, but is it correct? It doesn’t matter. You can always change the patterns later. Also, you don’t need to implement messages you aren’t using yet.

Now that you have some initial messages, you can think about their interactions. Let’s assume you have a web server handling inbound HTTP requests on the external side and generating microservice messages on the internal side:

  • When the user posts a new entry, the web server sends a post:entry message, which triggers a store:save,kind:entry message.
  • When the user lists their previous entries, the web server sends a store:list, kind:entry message to get this list.

Here’s another thing to think about: are these messages synchronous or asynchronous? In more-concrete terms, does the sender of the message expect to get a response (synchronous), or does the sender not care about a response (asynchronous)?

  • post:entry is synchronous, because it’s nice for the user to get confirmation that their entry has been posted.
  • store:save,kind:entry is also synchronous, because it has to provide confirmation of the save operation and probably returns the generated unique identifier of the new data record.
  • store:list,kind:entry is necessarily synchronous, because its purpose is to return a result list.

Are there any asynchronous messages in this simple first iteration? As a rule of thumb, microservice systems often benefit from announcement messages. That is, you should let the world know that something happened, and the world can decide if it cares. This suggests another kind of message:

  • info:entry is asynchronous and announces that a new entry has been posted. No reply is expected. There may be a microservice out there that cares, or maybe nobody cares.

These architectural musings lead you to tabulate for your two activities the message flows shown in table 1.1.

Table 1.1. Business activities and their associated message flows

Activity

Message flow

Post entry 1 post:entry 2 store:save,kind:entry 3 info:entry
List entry 4 store:list,kind:entry

You haven’t even thought about microservices yet. By thinking about messages first, you’ve avoided falling into the trap of working on the implementation before understanding what you need to build.

At this point, you have enough to go on, and you can group messages into sensible divisions, suggesting the appropriate microservices to build in this iteration. Here are the microservices:

  • front— The web server that handles HTTP requests. It sits behind a traditional load balancer.
  • entry-store— Handles persistence of entry data.
  • post— Handles the message flow for posting an entry.

Each microservice sends and receives specific messages, which you can tabulate as shown in table 1.2. The diagram in figure 1.1 shows how this all fits together.

Table 1.2. Messages that each microservice sends and receives

Microservice

Sends

Receives

front post:entry store:list,kind:entry  
entry-store   store:list,kind:entry store:save,kind:entry
post store:save,kind:entry info:entry post:entry
Figure 1.1. Iteration 0: Messages and services that support posting and listing entries

Here are the architectural decisions you’ve made:

  • There’s a traditional load balancer in front of everything.
  • The web server is also a microservice (front) and participates in the message flows. It doesn’t accept messages from external clients, only proxied HTTP requests from the load balancer.
  • The front service should be considered the boundary of the system. It translates HTTP requests into internal messages.
  • The entry-store microservice exposes a data store, but only via messages. No other microservice can access the underlying database.
  • The post service orchestrates the message flow that implements posting an entry. First it performs the synchronous store:save,kind:entry; once it has a confirmed save, it emits an asynchronous info:entry.

This little microblogging system allows users to post entries and see a list of their previous entries. For now, assume deployment is fully automated; chapter 5 covers deployment of microservices. It’s Friday, you’ve pushed code, and you can go home.

1.2.2. Iteration 1: A search index

In the last iteration, you used a method of system design that works very well for microservice architectures. First, you informally described the activities in the system. Then, you represented those activities as messages. Finally, you derived services from the messages. Messages, it turns out, are more important than services.

The task in this iteration is to introduce a search index so that users can search through entries to find wonderful gems of microblogging wit and insight amid a sea of sentiment. Much like a database, the system will use a search engine running inside the network to provide the search functionality. This suggests a microservice to expose the search engine, similar to the way the entry database is exposed by the entry-store microservice.

But we’re getting ahead of ourselves. First, what are the messages? Users can add entries to the search engine, and they can query the search engine. That’s enough for now. This gives you the following message patterns:

  • search:insert—Inserts an entry
  • search:query—Performs a query, returning a list of results

The front microservice can issue search:query messages to get search results. That seems OK. The post microservice can orchestrate a search:insert message into its entry-posting workflow. That doesn’t seem OK. Didn’t you introduce an info:entry asynchronous message for exactly the purpose of acting on new entries? The search engine microservice—let’s call it index—should listen for info:entry and then insert the new entry into its search index. That keeps the post and index microservices decoupled (almost).

Something still isn’t right. The problem is that the index microservice is concerned only with activities related to search—why should it know anything about posting microblogging entries? Why should index know that it has to listen for info:entry messages? How can you avoid this semantic coupling?

The answer is translation. The business logic of the index microservice shouldn’t know about microblogging entries, but it’s fine if the runtime configuration of the index microservice does.

The runtime configuration of index can listen for info:entry messages and translate them into search:insert messages locally. Loose coupling is preserved. The ability to perform this type of integration, without creating the tight coupling that accumulates technical debt, is the payoff you’re seeking from microservices. The business logic implementing a microservice can have multiple runtime configurations, meaning it can participate in the system in many ways, without requiring changes to the business-logic code.

Table 1.3 shows the new list of services and their message allocations.

Table 1.3. Messages that each microservice sends and receives

Microservice

Sends

Receives

front post:entry store:list,kind:entry search:list  
entry-store   store:list,kind:entry store:save,kind:entry
post store:save,kind:entry info:entry post:entry
index   search:query search:insert info:entry

Now, you’ll also apply the principle of additivity. To your live system, running in production, you’ll deploy the new index microservice. It starts listening for info:entry messages and adding entries to the search index. This has no effect on the rest of the system. You review your monitoring system and find that clients still experience good performance and nothing has broken. Your only action has been to add a new microservice, leaving the others running. There’s been no downtime, and the risk of breakage was low.

You still need to support the search functionality, which requires making changes to an existing microservice. The front service needs to expose a search endpoint and display a search page. This is a riskier change. How do you roll back if you break something? In a traditional monolithic[7] architecture, teams often use a blue-green configuration, where two copies (blue and green) of the entire system are kept running at the same time. Only one is live; the other is used for deployments. Once validated, the systems are switched. If there’s a glitch, you can roll back by switching the systems again. This is a lot of overhead to set up and maintain.

7

The term monolith stands for enterprise systems with large volumes of code that run as one process.

Consider the microservice case, where you can use the property of additivity. You deploy a new version of the front microservice—one that can handle searches—by starting one or more instances of the new version of front alongside running instances of the old version of front. You now have multiple versions in production, but you haven’t broken anything, because the new version of front can handle entry posting and listing just as well as the old version. The load balancer splits traffic evenly between the new and old versions of front. You have the option to adjust this if you want to be extra careful and use the load balancer to send only a small amount of traffic to the new version of front. You get this capability without having to build it into the system; it’s part of the deployment configuration.

Again, you monitor, and after a little while, if all is well, you shut down the running instances of the old version of front. Your system has gained a new feature by means of you adding and removing services, not via global modification and restart. Yes, you did “modify” the front service, but you could treat the new version as an entirely new microservice and stage its introduction into the live system. This is very different from updating the entire system and hoping there are no unforeseen effects in production. The new system is shown in figure 1.2.

Figure 1.2. Iteration 1: Adding messages and services that support searching entries

Consider table 1.4, which lists the series of small, safe deployment steps that got you here from the iteration 0 system. Another successful week! It’s Friday, and you can go home.

Table 1.4. A sequence of small deployment steps

Step

Microservice/Version

Action

0 index/1.0 Add
1 front/2.0 Add
2 front/1.0 Remove

1.2.3. Iteration 2: Simple composition

Microservices are supposed to be components, and a good component model enables composition of components. Let’s see how this works in a microservice context. The entry-store microservice loads data from an underlying database. This operation has a relatively high latency—it takes time to talk to the database. One way to improve perceived performance is to decrease latency, and one way to do that is to use a cache. When a request comes in to load a given entry, you check the cache first, before performing a database query.

In a traditional system, you’d use an abstraction layer within the code base to hide the caching interactions. In particularly bad code bases, you may have to refactor first to even introduce an abstraction layer. As a practical matter, you have to make significant changes to the logic of the code and then deploy a new version of the entire system.

In the little microservice architecture, you can take a different road: you can introduce an entry-cache microservice that captures all the messages that match the pattern store:*,kind:entry. This pattern matches all the data-storage messages for entries, such as store:list,kind:entry and store:load,kind:entry. The new microservice provides caching functionality: if entry data is cached, return the cached data; if not, send a message to the entry-store service to retrieve it. The new entry-cache microservice captures messages intended for the existing entry-store.

There’s a practical question here: how does message capture work? There’s no single solution, because it depends on the underlying message transportation and routing.

One way to do message capture is to introduce an extra property into the store:* messages—say, cache:true. You tag the message with a property that you’ll use for routing. Then, you deploy a new version (2.0) of the entry-store service that can also listen for this pattern. By “new version,” I mean only that the runtime message-routing configuration has changed. Then, you deploy the entry-cache service, which listens for store:* as well, but sends store:*,cache:true when it needs original data:

  • entry-store— Listens for store:* and store:*,cache:true
  • entry-cache— Listens for store:*, and sends store:*,cache:true

The other services then load-balance store:* messages between these two services[8] and receive the same responses as before, with no knowledge that 50% of messages are now passing through a cache.

8

Let’s assume this “just works” for now. The example code at www.manning.com/books/the-tao-of-microservices and http://ramanujan.io has all the details, of course.

Finally, you deploy another new version (3.0) of entry-store that only listens for store:*,cache:true. Now, 100% of store:* messages pass through the cache:

  • entry-store— Listens for store:*,cache:true only
  • entry-cache— Listens for store:*, and sends store:*,cache:true

You added new functionality to the system by adding a new microservice. You did not change the functionality of any existing service.

Table 1.5 shows the deployment history.

Table 1.5. Modifications to the behavior of entry-store over time

Step

Microservice/Version

Action

Message patterns

0 entry-store/2.0 Add store:* and store:*,cache:true
1 entry-store/1.0 Remove store:*
2 entry-cache/1.0 Add store:*
3 entry-store/3.0 Add store:*,cache:true
4 entry-store/2.0 Remove store:* and store:*,cache:true

You can see that the ability to perform deployments as a series of add and remove actions gives you fine-grained control over your microservice system. In production, this ability is an important way to manage risk, because you can validate the system after each add or remove action to make sure you haven’t broken anything. Figure 1.3 shows the updated system.

Figure 1.3. Iteration 2: An entry store cache implemented by message capture

The message-tagging approach assumes you have a transport system where each microservice can inspect every message to see whether the message is something it can handle. For a developer, this is a wonderfully useful fiction. But in practice, this isn’t something you want to do, because the volume of network traffic, and the work on each service, would be too high. Just because you can assume universal access to all messages as a microservice developer doesn’t mean you have to run your production system this way. In the message-routing layer, you can cheat, because you already know which messages which microservice cares about—you specified them!

You’ve composed together the caching functionality of entry-cache and the data-storage functionality of entry-store. The rest of the world has no idea that the store:*, kind:entry messages are implemented by an interaction of two microservices. The important thing is that you were able to do this without exposing internal implementation details, and the microservices interact only via their public messages.

This is powerful. You don’t have to stop at caching. You can add data validation, message-size throttling, auditing, permissions, and all sorts of other functionality. And you can deliver this functionality by composing microservices together at the component level. The ability to do fine-grained deployment is often cited as the primary benefit of microservices, but it isn’t. The primary benefit is composition under a practical component model.

Another successful week.

1.2.4. Iteration 3: Timelines

The core feature of a microblogging framework is the ability to follow other users and read their entries. Let’s implement a Follow button on the search result list, so that if a user sees somebody interesting, they can follow that person. You’ll also need a home page for each user, where they can see a timeline of entries from all the other users they follow. What are the messages?

  • follow:user—Follows somebody
  • follow:list,kind:followers|following—Lists a user’s followers, or who they’re following
  • timeline:insert—Inserts an entry into a user’s timeline
  • timeline:list—Lists the entries in a user’s timeline

This set of messages suggests two services: follow, which keeps track of the social graph (who is following who), and timeline, which maintains a list of entries for each user, based on who they follow.

You aren’t going to extend the functionality of any existing services. To add new features, you’ll add new microservices. This avoids technical debt by moving complexity into the message-routing configuration and out of conditional code and intricate data structures.

Using pattern matching to route messages can handle this complexity more effectively than programming language structures because it’s a homogeneous representation of the business activities you’re modelling. The representation consists only of pattern matching on messages and using these patterns to assign them to microservices. Nothing more than simple pattern matching is needed. You understand the system by organizing the message patterns into a hierarchy, which is much easier to comprehend than an object-relationship graph.

At this point, you face an implementation question: should timelines be constructed in advance or on demand? To construct a timeline for a user on demand, you’d have to get the list of users that the user follows—the user’s “following” list. Then, for each user followed, you’d need to get a list of their entries and merge the entries into a single timeline. This list would be hard to cache, because the timeline changes continuously as users post new entries. This doesn’t feel right.

On the other hand, if you listen for info:entry messages, you can construct each timeline in advance. When a user posts an entry, you can get the list of their followers; then, for each follower, you can insert the entry into the follower’s timeline. This may be more expensive, because you’ll need extra hardware to store duplicate data, but it feels much more workable and scalable.[9] Hardware is cheap.

9

Timeline insertion is how the real Twitter works, apparently. A little bird told me.

Building the timelines in advance requires reacting to an info:entry message with an orchestration of the follow:list,kind:followers and timeline:insert messages. A good way to do orchestration is to put it into a microservice built for exactly that purpose. This keeps intelligence at the edges of the network, which is a good way to manage complexity. Instead of complex routing and workflow rules for everything, you understand the system in terms of the inbound and outbound message patterns for each service. In this case, let’s introduce a fanout service that handles timeline updates. This fanout service listens for info:entry messages and then updates the appropriate timelines. The updated system with the interactions of the new fanout, follow, and timeline services is shown in figure 1.4.

Figure 1.4. Iteration 3: Adding social timelines

Both the follow and timeline services store persistent data, the social graph, and the timelines, respectively. Where do they store this data? Is it in the same database that the entry-store microservice uses? In a traditional system, you end up putting most of your data in one big, central database, because that’s the path of least resistance at the code level. With microservices, you’re freed from this constraint. In the little micro-blogging system, there are four separate databases:

  • The entry store, which is probably a relational database
  • The search engine, which is definitely a specialist full-text search solution
  • The social graph, which might be best handled by a graph database
  • The user timelines, which can be handled by a key-value store

None of these database decisions are absolute, and you could certainly implement the underlying databases using different approaches. The microservices aren’t affected by each other’s choice of data store, and this makes changes easier. Later in the project, if you find that you need to migrate to a different database, then the impact of that change will be minimized.

At this point, you have a relatively complete microblogging service. Another good Friday for the team!

1.2.5. Iteration 4: Scaling

This is the last iteration before the big pitch for a series A venture capital round. You’re seeing so much traction that you’re definitely going to get funded. The trouble is, your system keeps falling over. Your microservices are scaling fine, because you can keep adding more instances, but the underlying databases can’t handle the data volumes. In particular, the timeline data is becoming too large for one database, and you need to split the data into multiple databases to keep growing.

This problem can be solved with database sharding. Sharding works by assigning each item of data to a separate database, based on key values in the data. Here’s a simplistic example: to shard an address book into 26 databases, you could shard on the first letter of a person’s name. To shard data, you’d typically rely on the specific sharding capabilities of a database driver component, or the sharding feature of the underlying database. Neither of these approaches is appealing in a microservice context (although they will work), because you lose flexibility.

You can use microservices to do the sharding by adding a shard property to the timeline:* messages. Run new instances of the timeline service, one group for each shard, that react only to messages containing the shard property. At this point, you have the old timeline service running against the old database, and a set of new sharding timeline services. The implementation of both types of timeline is the same; you’re just changing the deployment configuration and pointing some instances at new databases.

Now, you migrate over to the sharding configuration by using microservice composition. Introduce a new version of the timeline microservice, which responds to the old timeline:* messages that don’t have a shard property. It then determines the shard based on the user, adds a shard property, and sends the messages onward to the sharded timeline microservices. This is the same structure as the relationship between entry-cache and entry-store.

There are complications. You’ll be in a transitional state for a while, and during this period you’ll have to batch-transfer the old data from the original database into the new database shards. Your new timeline microservice will need logic to look for data in the old database if it can’t find it in the new shard. You’ll want to move carefully, leaving the old database active and still receiving data until you’re sure the sharding is working properly. You should probably test the whole thing against a subset of users first. This isn’t an easy transition, but microservices make it less risky. Far more of the work occurs as careful, small steps—simple configuration changes to message routing that can be easily rolled back. It’s a tough iteration, but not something that brings everything else to a halt and requires months of validation and testing.[10] The new sharding system is shown in figure 1.5.

10

Sharding using microservices is by no means the “best” way. That depends on your own judgment as an architect for your own system. But it’s possible, using only message routing, and it’s a good example of the flexibility that microservices give you.

Figure 1.5. Iteration 3: Scaling by sharding at the message level

Now, let’s assume that everything goes according to plan, and you get funded and scale up to hundreds of millions of users. Although you’ve solved your technology problems, you still haven’t figured out how to monetize all those users. You take your leave as you roll out yet another advertising play—at least it’s quick to implement using microservices!

Wouldn’t it be great if enterprise software development worked like this? The vast majority of software developers don’t have the freedom that startup software development brings, not because of organizational issues, but because they’re drowning in technical debt. That’s a hard truth to accept, because it requires honest reflection on our effectiveness as builders of software.

The microservice architecture offers a solution to this problem based on sound engineering principles, not on the latest project management or technology platform fashions. Yes, there are trade-offs, and yes, it does require you to think in a new way. In particular, the criticism that complexity is being moved around, not reduced, must be addressed. It isn’t so. Message flows (being one kind of thing) are more understandable than the internals of a monolith (being many kinds of things). Microservices interact only by means of messages and are completely defined by these interactions. The internal programming structures of monoliths interact in all sorts of weird and wonderful ways, and offer only weak protection against interference with each other.

A microservice system can be fully understood at an architectural level by doing the following:

  1. Listing the messages. These define a language that maps back to business activities.
  2. Listing the services and the messages they send and receive.

On a practical level, yes, you do need to define and automate your deployment configuration. That is a best practice and not hard to do with modern tools. And you need to monitor your system—but at the level of messages and their expected flows. This is far more useful than monitoring the health of individual microservice instances.

The case study is concluded. Let’s review some of the concepts that underlie the microservice approach.

1.3. How the monolith betrays the promise of components

When I talk about monoliths in this book, I mean large, object-oriented systems[11] developed within, and used by, large organizations. These systems are long-lived, under constant modification to meet ongoing business requirements, and essential to the health of the business. They’re layered, with business logic in all the layers, from the frontend down to the database. They have wide and deep class hierarchies that no longer represent business reality accurately. Complex dependencies between classes and objects have arisen in response. Data structures not only suffer from legacy artifacts but must be twisted to meet new models of the world and are translated between representations with varying degrees of fidelity. New features must touch many parts of the system and inevitably cause regressions (new versions break previously working features).

11

The essential characteristic of a monolith isn’t that a large body of code executes inside a single process. It’s that the monolith makes full use of its language platform to connect separate functional elements, thereby mortally wounding the composability of those elements as components. The object-oriented nature of these systems and the fact that they form the majority of enterprise software are mostly due to historical accident. There are other kinds of monoliths, but the object-oriented architecture has been such a pretender to the crown of software excellence, particularly in its broken promise to deliver reusable components, that it’s the primary target of this book’s ire.

The components of the system have lost encapsulation. The boundaries between components are shot full of holes. The internal implementation of components has become exposed. Too many components know about too many other components, and dependency management has become difficult. What went wrong? First, object-oriented languages offer too many ways to interfere with other objects. Second, every developer has access to the entire code base and can create dependencies faster than the poor architects can shoot them down. Third, even when it’s clear that encapsulation is suffering, there’s often no time for the refactoring necessary to preserve it. And the need for constant refactoring compounds the problem. This is a recipe for ballooning technical debt.

The components don’t deliver on reusability. As a consequence of losing encapsulation, they’re deeply connected to other components and difficult to extract and use again. Some level of reuse can be achieved at the infrastructure level (database layers, utility code, and the like). The real win would be to reuse business logic, but this is rarely achieved. Each new project writes business logic anew, with new bugs and new technical debt.

The components don’t have well-defined interfaces. Certainly, they may have strict interfaces with a great deal of type safety, but the interfaces are too intricate to be well defined. There’s a combinatorial explosion of possibilities when you consider the many ways in which you can interact with an object: construction dependencies, method calls, property access, and inheritance; and that’s not even counting the extra frills that any given language platform might give you. And to use the object properly, you need to build a mental model of its state, and the transitions of that state, giving the entire interface an additional temporal dimension.

Components don’t compose, betraying the very thing for which they were named. In general, it’s hard to take two objects and combine them to create enhanced functionality. There are special cases, such as inheritance and mixins,[12] but these are limited and now considered harmful. Modern object-oriented best practice is explicit: favor composition over inheritance. Keep an internal instance of what would have been your superclass, and call its methods directly. This is better, but you still need too much knowledge of the internal instance.

12

Inheritance was to be the primary mechanism of composition for object-oriented programming. It fails because the coupling between superclass and subclass is too tight, and it’s tricky to subclass from more than one superclass. Multiple inheritance (alternatively, mixins) as a solution introduces more combinatorial complexity.

The problem with the object-oriented model is that universal composition is difficult to do well as an afterthought of language design. It’s much better to design it in from the start so that it always works the same way and has a small, consistent, predictable, stateless, easily understood implementation model. A compositional model has hit the bull’s-eye when it can be defined in a fully declarative manner, completely independent of the internal state of any given component.

Why is composition so important? First, it’s one of the most effective conceptual mechanisms we have for managing complexity. The problem isn’t in the machine; the problem is in our heads. Composition imposes a strict containment of complexity. The elements of the composition are hidden by the result of the composition and aren’t accessible. There’s no way to create interconnected spaghetti code, because composition can only build strict hierarchies. Second, there’s significantly less shared state. By definition, composed components communicate with each other via a stateless model, which reduces complexity. Peer communication between components is still subject to all the nastiness of traditional object communication. But a little discipline in reducing the communication mechanisms, perhaps by limiting them to message passing, can work wonders. Reducing the impact of state management means the temporal axis of complexity is much less of a concern. Third, composition is additive.[13] You create new functionality by combining existing functionality. You don’t modify existing components. This means technical debt inside a component doesn’t grow. Certainly, there’s technical debt in the details of the composition, and it grows over time. But it’s necessarily less than the technical debt of a traditional monolith, which has the debt of compositional kludges, the debt of feature creep within components, and the debt of increasing interconnectedness.

13

A system is additive when it allows you to provide additional implementations of functionality without requiring changes in those that depend on you. For a technical description, see Harold Abelson, Gerald Sussman, and Julie Sussman, Structure and Interpretation of Computer Programs (MIT Press, 1996), section 2.4.

What is complexity?

You might say that the fraction 111/222 is somehow more complex that the fraction 1/2.[14] You can make that statement rigorous by using the Kolmogorov complexity measure. First, express the complex thing as a binary string by choosing a suitable encoding. (I realize I’m leaving entire industries, like recorded music, as an exercise for you to solve, but bear with me!) The length of the shortest program that can output that binary string is a numeric measure of the complexity of the thing. Before you start, choose some implementation of a universal Turing machine so the idea of a “program” is consistent and sensible.

14

Mandelbrot Set fractal is a good example of low complexity. It can be generated from a simple recurrence formula on complex numbers: http://mathworld.wolfram.com/MandelbrotSet.html. For details on Kolmogorov complexity, start here: https://en.wikipedia.org/wiki/Kolmogorov_complexity

Here are the two programs, in (unoptimized!) colloquial C. One prints the value of 111/222:

printf("%f", 111.0/222.0);

And the other print the value of 1/2:

printf("%f", 1.0/2.0);

Feel free to compile and compress them any way you like. It will always take more bits to express the values 111 and 222 than it will to express 1 and 2 (sorry, compiler optimizations don’t count). Thus, 111/222 is more complex than 1/2. This also satisfies our intuition that fractions can be reduced to their simplest terms and that this is a less complex representation of the fraction.

A software system that combines elements in a small number of ways (additive composition) compared to a system that can combine elements in many ways (object orientation) grows in complexity far more slowly as the number of elements increases. If you use complexity as a proxy measure of technical debt, you can see that object-oriented monoliths are more vulnerable to technical debt.

How much more vulnerable? Very much more! The total possible interactions between elements grows exponentially with the number of elements and the number of ways of connecting them.[15]

15

For the mathematically inclined, k^(n(n–1)/2), where k is the number of ways to connect elements and n is the number of elements. This model is a worst case, because the graph of object dependencies isn’t quite as dense, but it’s still pretty bad.

1.4. The microservice idea

Large-scale software systems are best built using a component architecture that makes composition both possible and easy. The term microservice captures two important aspects of this idea. The prefix micro indicates that the components are small, avoiding accumulation of technical debt. In any substantial system, there will be many small components, rather than fewer large ones. The root service indicates that the components shouldn’t be constrained by the limitations of a single process or machine and should be free to form a large network. Components—that is, microservices—can communicate with each other freely. As a practical matter, communication via messages, rather than shared state, is essential to scaling the network.

That said, the heart of the microservice idea is bigger than these two facets. It’s the more general idea that composable components are the units of software construction, and that composition works well only when the means of communication between components is sufficiently uniform to make composition practical. It isn’t the choice of the mechanism of communication that matters. What matters is the simplicity of the mechanism.

The core axioms of the microservice architecture are as follows:

  • No components are privileged (no privilege).
  • All components communicate in the same simple, homogeneous way (uniform communication).
  • Components can be composed from other components (composition).

From these axioms proceed the more concrete features of the microservice architecture. Microservices in practice are small, because the smaller a service is, the easier it is to compose. And if it’s small, its implementation language matters less. In fact, a service may be disposable, because rewriting its functionality doesn’t require much work. And pushing even further, does the quality of its code really matter? We’ll explore these heresies in chapter 8.

Microservices communicate over the network using messages. In this way, they share a surface feature with service-oriented architectures. Don’t be fooled by the similarity; it’s immaterial to the microservice architecture what data format the messages use or the protocols by which they’re transported. Microservices are entirely defined by the messages they accept and the messages they emit. From the perspective of an individual microservice instance, and from the perspective of the developer writing that microservice, there are only messages arriving and messages to send. In deployment, a microservice instance may be participating in a request/response configuration, a publish/subscribe configuration, or any number of variants. The way in which messages are distributed isn’t a defining characteristic of the microservice architecture. All distribution strategies are welcome without prejudice.[16]

16

Many criticisms of the microservice architecture use the straw man argument that managing and understanding hordes of web server instances exposing HTTP REST APIs hurts too much. Well, if it hurts, stop doing it!

The messages themselves need not be strictly controlled. It’s up to the individual microservice to decide whether an arriving message is acceptable. Thus, there’s no need for schemas or even validation. If you’re tempted to establish contracts between microservices, think again. All that does is create a single large service with two separate, tightly coupled parts. This is no different from traditional monolithic architectures, except that now you have the network to deal with as well.[17] Flexibility in the structure of messages makes composition much easier to achieve and makes development faster, because you can solve the simple, general cases first and then specialize with more microservices later. This is the power of additivity: technical debt is contained, and changing business requirements can be met by adding new microservices, not modifying (and breaking) old ones.

17

The appropriate pejorative is “Distributed monolith!”

A network of microservices is dynamic. It consists of large numbers of independent processes running in parallel. You’re free to add and remove services at will. This makes scaling, fault tolerance, and continuous delivery practical and low risk. Naturally, you’ll need some automation to control the large network of services. This is a good thing: it gives you control over your production system and immunizes you against human error. Your default operational action is to add or remove a single microservice instance and then to verify that the system is still healthy. This is a low-risk procedure, compared to big-bang monolith deployments.

1.4.1. The core technical principles

A microservice network can deliver on the axioms by meeting a small set of technical capabilities. These are transport independence and pattern matching, which together give you additivity.

Transport independence

Transport independence is the ability to move messages from one microservice to another without requiring microservices to know about each other or how to send messages. If one microservice needs to know about another microservice and its message protocol in order to send it a message, this is a fatal flaw. It breaks the no privilege axiom, because the receiver is privileged from the perspective of the sender. You’re no longer able to compose other microservices over the receiver without also changing the sender.

There are degrees of transport independence. Just as all programming languages are executed ultimately as machine code, all message-transport layers must ultimately resolve senders and receivers to exact network locations. The important factor is how much information is exposed to the internal business logic of the microservice to allow it to send a message. Different message transports (shown in figure 1.6) have different levels of coupling.

Figure 1.6. The underlying infrastructure that moves messages shouldn’t be known to microservices.

For example, requiring a microservice to look up another service using a service registry is asking for trouble, because doing so creates a dangerous coupling. But service discovery need not be so blatant. You can hide services behind load balancers. The load balancers must know where the services are located, so you haven’t solved the problem; but you’ve made services easier to write, because they only need to find the load balancer, and they’ve become more transport independent. Another approach is to use message queues. Your service must still know the correct topics for messages, and topics are a weak form of network address. The extreme degree, where microservices know nothing of each other, is the goal, because that gives you the full benefits of the architecture.

Pattern matching

Pattern matching is the ability to route messages based on the data inside the messages.[18] This capability lets you dynamically define the network. It allows you to add and remove, on the fly, microservices that handle special cases, and to do so without affecting existing messages or microservices. For example, suppose the initial version of your enterprise system has generic users; but later, new requirements mean you have different kinds of users, with different associated functionalities. Instead of rewriting or extending the generic user-profile service (which still does the job perfectly well for generic users), you can create a new user-profile service for each kind of user. Then, you can use pattern matching to route user profile request messages to the appropriate service. The power of this approach to reduce technical debt is evident from the fact that no knowledge or dependency exists between different user profile services—they all think that only their kind of user exists. Figure 1.7 shows how the user profile functionality is extended by new microservices.

18

Message routing is free to use any available contextual information to route messages and isn’t necessarily limited to data within the messages. For example, microservices under high load can use back-pressure alerts to force traffic onto microservices with less load.

Figure 1.7. Introducing support for new user profiles

Pattern matching is also subject to degrees of application. The allocation of separate URL endpoints to separate microservices via a load balancer matching against HTTP request paths is an example of a simple case. A full-scale enterprise service bus with myriad complex rules is the extreme end case. Unlike transport independence, the goal isn’t to reach an extreme. Rather, you must seek to balance effectiveness with simplicity. You need pattern matching that’s deep enough to express business requirements yet simple enough to make composition workable. There’s no right answer, and the uniform communication axiom’s exhortation to keep things simple and homogeneous reflects that. This book proposes that URL matching isn’t powerful enough and provides examples of slightly more powerful techniques. There are many ways to shoot yourself in the foot, and it’s easy to end up with systems that are impossible to reason about. Err on the side of simplicity, and preserve composability!

Additivity

Additivity is the ability to change a system by adding new parts (see figure 1.8). The essential constraint is that other parts of the system must not change. Systems with this characteristic can deliver very complex functionality and be very complex themselves, and yet maintain low levels of technical debt.[19] Technical debt is a measure of how hard it is to add new functionality to a system. The more technical debt, the more effort it takes to add functionality. A system that supports additivity is one that can support ongoing, unpredictable changes to business requirements. A messaging layer based on pattern matching and transport independence makes additivity much easier to achieve because it lets you reorganize services dynamically in production.

19

The venerable Emacs editor is an example of such a system. It’s incredibly easy to extend, despite being a relic of the 1970s. LISP supports additivity.

Figure 1.8. Additivity allows a system to change in discrete, observable steps.

There are degrees of additivity. A microservice architecture that relies on the business logic of microservices to determine the destination of messages will require changes in both the sender and receiver when you add new functionality. A service registry won’t help you, because you’ll still need to write the code that looks up the new service. Additivity is stronger if you use intelligent load balancers, pattern matching, and dynamic registration of new upstream receivers to shield senders from changes to the set of receivers. Message bus architectures give you the same flexibility, although you’ll have to manage the topic namespace carefully. You can achieve near-perfect additivity using peer-to-peer service discovery.[20] By supporting the addition of new microservices and allowing them to wrap, extend, and alter messages that are inbound or outbound to other services, you can satisfy the composition axiom.

20

The SWIM algorithm, which provides efficient dissemination of group membership over a distributed network, is an example of the work emerging in this space. See Abhinandan Das, Indranil Gupta, and Ashish Motivala, “SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol,” Proceedings of the International Conference on Dependable Systems and Networks (2002), www.cs.cornell.edu/~asdas/research/dsn02-swim.pdf.

1.5. Practical implications

The practice of enterprise development is riven with doublethink.[21] The accumulation of technical debt is the most politically expedient short-term success strategy. The organization simultaneously demands perfect software while imposing constraints that can’t be satisfied. Everybody understands the consequences yet participates in what might be called “quality theater,” because to do otherwise is a career-limiting move. The expectation of perfection, without understanding its true cost, is the root cause of most software development headaches.[22] Both developers, chasing intellectual highs by building castles in the sky, and the business, lacking an engineering mindset to make trade-offs, are guilty.

21

A fabulously useful concept from George Orwell’s 1984: “The power of holding two contradictory beliefs in one’s mind simultaneously, and accepting both of them.”

22

The cost of the space shuttle software system, one of the most defect-free code bases ever written, has been estimated to be at least $1,000 per line of code. See Charles Fishman, “They Write the Right Stuff,” Fast Company, December 31, 1996, www.fastcompany.com/28121/they-write-right-stuff. The doublethink of enterprise software development is that space shuttle quality is possible without space shuttle spending.

Sometimes, this problem can be solved with an edict from on high. Facebook’s infamous mantra to “move fast and break things” implicitly acknowledges that perfection is too expensive to justify. Enterprise software developers don’t often have the option to use this approach. The more usual business solution is to externalize the problem by converting it into high-stress software development death marches.

The microservice architecture is both a technical and a political strategy. It takes a cannon to intellectual castles and demands honesty from the business. The payoff includes regular work hours and accelerated delivery of business goals. From a technical standpoint, it removes many opportunities for technical debt to accumulate and makes large, distributed teams easier to manage. From a business standpoint, it forces acceptance of system failures and defects. A more honest discussion of the acceptable levels of failure can then begin, and business leaders can make accurate decisions about return on investment in the face of quantified risks. This is something they’re good at, given the chance.

1.5.1. Specification

Given that microservices are different from traditional architectures, how do you specify such systems? The first thing to realize is that microservices aren’t a radical or revolutionary approach. The architecture emphasizes a component-oriented mindset. As developers, this allows us to ignore language frills. If class hierarchies, object relationships, and data schemas were so effective, we wouldn’t be firing blanks all the time, would we?

Microservice system design is very direct. You take the informal business requirements,[23] determine the behaviors of the system, and map the behaviors to messages. This is a messages-first approach. Ironically, designing a microservice system doesn’t start by asking what services to build; it starts by asking what messages the services will exchange. Once you have the messages, natural groupings will suggest the services to build.

23

Business requirements aren’t made formal by accumulating detail. Better to avoid overspecification and stay focused on the desired business outcomes.

Why do things this way? You have a direct route from business requirements to implementation. This route is traceable and even measurable, because message behavior in production represents desired business outcomes. Your design is independent of implementation, deployment, and data structures. This flexibility allows you to keep up with changing business requirements. Finally, you get a domain language—the list of messages is a language that describes the system. From this language, you can derive shared understanding, quality expectations, and performance constraints, and validate correctness in production. All of this is much easier than traditional systems, because messages are homogeneous: they’re always the same kind of thing.[24]

24

Non sunt multiplicanda entia sine necessitate, otherwise known as Occam’s razor, is the philosophical position that entities must not be multiplied beyond necessity. It puts the onus on those adding complexity to justify doing so. Object-oriented systems provide lots of ways for objects to interact, without much justification.

The practice of microservice specification is guided by a single key principle: move from the general to the specific. First, you solve the simplest general problem you can. Then, you add more microservices to handle special cases. This approach preserves additivity and delivers the core benefit of microservices: lower technical debt. Let’s say you’re building an internal employee-management system for a global organization. You can start with the simple case of local employees and local regulations. Then, you add microservices for regional community rules, such as those for the United States or the EU. Finally, you handle each country as a special case. As the project progresses, you can deliver more and more value and handle larger and larger subsets of the employee base. Compare this approach to the more common attempt to design sufficiently complex and flexible data structures to handle the entire employee base. Inevitable exceptions will disrupt the integrity of the initial design, ricocheting off false assumptions to cause the collateral damage of technical debt.

1.5.2. Deployment

The systems-management needs of microservices are presented as a weakness of the architecture. It’s true that managing the deployment of many hundreds or thousands of individual services in production isn’t a trivial task; but this is a strength, not a weakness, because you’re forced to automate the management of your system in production. Monoliths are amenable to manual deployment, and it’s just about possible to get by. But manual deployment is extremely risky and the cause of a great deal of stress for project teams.[25] The need to automate the deployment of your microservices, and the need to face this requirement early, raises the professionalism of the entire project, because another hiding place for sloppy practices is shot down.

25

On the morning of Wednesday, August 1, 2012, Knight Capital, a financial services firm on the New York Stock Exchange, lost $460 million in 45 minutes. The firm used a manual deployment process for its automated trading system and had erroneously updated only seven or eight production servers. The resulting operation of the new and old versions of the system, with mismatched configuration files, triggered a trading test sequence to execute in the real market. Automate your production systems! For a detailed technical analysis, see “In the Matter of Knight Capital Americas LLC,” Securities Exchange Act Release No. 34-70694, October 16, 2013, www.sec.gov/litigation/admin/2013/34-70694.pdf.

The volume of microservices in production also makes scaling much easier. By design, you’re already running multiple instances of many services, and you can scale by increasing the number of running instances. This scaling ability is more powerful than it first appears, because you can scale at the level of system capabilities. Unlike with monoliths, where scaling is all or nothing, you can easily have variable numbers of different microservices running, applying scaling only where needed. This is far more efficient. There’s no excuse for not doing this, and it isn’t a criticism of the microservice architecture to say that it’s difficult to achieve—this is a baseline feature of any reasonable deployment tool or cloud platform.

The fault tolerance of your overall system also increases. No individual microservice instance is that important to the functioning of the system as a whole. Taking this to an extreme, let’s say you have a high-load microservice that suffers from a memory leak. Any individual instance of this microservice has a useful lifetime of only 15 minutes before it exhausts its memory allocation. In the world of monoliths, this is a catastrophic failure mode. In the world of microservices, you have so many instances that it’s easy to keep your system running without any downtime or service degradation, and you have time to debug the problem in a calm, stress-free manner.[26]

26

This benefit is best expressed by Adrian Cockcroft, a former director of engineering at Netflix: “You want cattle, not pets.”

The monitoring of microservice systems is different from that of monoliths, because the usual measurements of low-level health, such as CPU load and memory usage, are far less significant. From a system perspective, what matters is the flow of messages. By monitoring the way messages flow through the system, you can verify that the business requirements and rules are being met, because there’s a direct mapping from behavior to messages. We’ll explore this in detail in chapter 6.

Monitoring message-flow rates is akin to the measurements that doctors use. Blood pressure and heart rate tell a physician much more about a patient that the ion-channel performance of any given cell. Monitoring at this higher level reduces deployment risk. You only ever change the system one microservice instance at a time. Each time, you can verify production health by confirming expected flow rates. With microservices, continuous deployment is the default way to deploy. The irony is that this mode of deployment, despite being far more frequent and subject to less testing, is far less risky than big-bang monolithic deployments, because its impact is so much smaller.

1.5.3. Security

It’s important to step away from the notion that microservices are nothing more than small REST web services. Microservices are independent of transport mechanism, and fixation on the security best practices of any one transport in itself creates exposure. As a guiding principle, external entry points to the system shouldn’t be presented in the same way as internally generated messages.

Microservices that serve external clients should do so explicitly, using the desired communication mechanism of the client directly. The requests of external clients shouldn’t be translated into messages and then presented to the microservice. There’s considerable temptation to do this, because it’s more convenient. A microservice that delivers HTTP content or serves an API endpoint should do so explicitly. Such a microservice may receive and emit internal messages in its interactions with other microservices, but at no time should there be any question of conflation. The microservice’s purpose is to expose an externally facing communication mechanism, which falls outside the communication model between microservices.

It shouldn’t be possible to tell from the outside that a system uses the microservice architecture. Security best practices for microservice systems are no different than for monoliths. The implementation of a hard and secure system boundary is always necessary and is independent of the internal network topology.

The microservice architecture must also deal with the additional exposure created by the fact that there are large numbers of network actors sending and receiving messages. Just because the boundary of the network is secure doesn’t mean communication between microservices doesn’t need to be secured.

Securing messages between services is best handled by a messaging layer that can provide the desired level of security. The messaging layer should handle certificate verification, shared secrets, access control, and other mechanisms of message validation. The need for security at this level casts a harsh light on naïve approaches to microservice communication. Using HTTP utility libraries directly within your microservices means you have to get the security configuration right every time, in each microservice.

Getting the security aspect right is tricky because you’ll have to work against a strict checklist from your enterprise security group. No such group is known for its compromising spirit. This is another advantage of the message abstraction layer—it gives you a place to put all the security implementation details, keeping them away from business logic. It also gives you a way to validate your security infrastructure independently of other layers of the system.

1.5.4. People

The software development process isn’t independent of the architecture under development. If you look closely, you’ll observe that the monolithic architecture drives many behaviors we’ve come to accept as necessary. For example, why are daily stand-ups so important to the agile process? Perhaps it’s because monolithic code bases are highly sensitive to unintended consequences. It’s easy for one developer’s work over here to break another’s over there. Careful branching rituals exist for the same reason. Open source projects that rely heavily on plugin architectures don’t seem to need stand-ups in the same way—perhaps because they use a component model.

The ceremonial demands of software development methodologies are dreamed up with the best of intentions. They’re all attempts to tame complexity and keep technical debt under control. Some methodologies focus on psychology, some on estimation, and others on a rigorous process. They may improve matters, but if any of them had a significant beneficial effect, we’d all adopt that practice without further discussion.[27] The counterargument is that most projects aren’t doing it right, and if only they would be disciplined in their approach, then they would be successful. One response is to observe that a methodology so fragile that most teams can’t execute it correctly is not fit for the purpose.

27

To use an example from another domain, the adoption of wheeled luggage by airline passengers was astonishing in the speed of its universal adoption.

Unit testing stands out as a practice that has seen wide adoption because it has clear benefits. This is even more of an indictment of other practices in the traditional methodologies of software development. Unfortunately, because it’s so effective, the unit-testing tail often wags the development dog. Projects impose global unit-testing requirements on all code—you must reach such and such a level of coverage, every method must have a test, you must create complex mock objects, and so forth. Stand back and ask these questions: Does all code need the same coverage? Is all code subject to the same production constraints? Some features deliver orders of magnitude more business value than others—shouldn’t you spend more time testing them and less time testing obscure features? The microservice architecture gives you a useful unit of division for quality. Different microservices can have different quality levels as a matter of deliberate choice. This is a more efficient way to allocate resources.

The microservice is also a unit of labor and thus a unit of estimation. Microservices are small, and it’s easier to estimate the amount of work required to build small things. This means more-accurate project estimates, especially if you can follow the strategy of moving from general to specific services over time. The older, more general services don’t change, so your estimation remains accurate in the later stages of the project, unlike more-traditional code bases where estimation accuracy deteriorates over time as internal coupling increases.

Finally, microservices are disposable. This is perhaps one of the greatest benefits of the architecture from a people perspective. Software developers are known for their large, fragile egos. There’s an economic explanation for this: it takes a great deal of effort to build a working monolith and a great deal of further investment to maintain it. This is accidental (rather than essential) knowledge, and it remains valuable only as long as the code base remains in production. From the perspective of a rational economic actor, there’s every incentive to keep old spaghetti code alive. Microservices don’t suffer from the same effect. They’re easy to understand by inspection—there isn’t much code. And they get replaced all the time; you write so many of them that individual ones aren’t that important. The microservice architecture cleans itself.

1.6. What you get for your money

Microservices can plausibly address the needs of custom enterprise software. By aligning the software architecture of the system more closely with the real intent of the business, software projects can have far more successful outcomes. Real business value is delivered more quickly. Microservice systems approach MVP status faster and thus can be put into production sooner. Once in production, they make it easier to keep up with changing requirements.

Waste and rework, also known as refactoring, are reduced because complexity within each microservice can’t grow to dangerous levels. It’s more efficient and easier to write new microservices to handle business change than it is to modify old ones. As a software developer, you can make a bigger professional impact with the evolutionary approach to systems building offered by microservices. It lets you be successful by making the best use of your time. It also frees you from the collective delusion that the building of intricate monolithic code can be accurately estimated. Instead, your value is clear from day one, and discussions with the business are of healthier variety, focused on how you can add value, not on gaming indirect measures of code volume.

In the next chapter, we’ll start to explore the technical foundations of the microservice architecture, starting with the nature of the services.

1.7. Summary

  • The easy accumulation of technical debt is the principle failure of the monolithic architecture.
  • Technical debt is caused by language platforms and architectures that make a high degree of coupling possible. Components know too much about each other.
  • The most effective way to hide components from each other is to compose them together. Composition allows you to make new components from existing ones, without accelerating the growth of complexity.
  • The microservice architecture provides a component model that provides a strong composition mechanism. This enables additivity, the ability to add functionality by adding new parts rather than modifying old ones.
  • The practical implications of the microservice architecture demand that you let go of some dearly held beliefs, like uniform code quality, and force acceptance of certain best practices, like deployment automation. These are all good things, because you end up with more efficient resource allocation.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset