Thursday, January 30, 2020

Guidelines for building evolutionary architectures

[...] As much as we like to talk about architecture in pristine, idealized settings, the real world often exhibits a contrary mess of technical debt, conflicting priorities, and limited budgets. Architecture in large companies is built like the human brain — lower-level systems still handle critical plumbing details but have some old baggage. Companies hate to decommission something that works, leading to escalating integration architecture challenges.

Retrofitting evolvability into an existing architecture is challenging — if developers never built easy change into the architecture, it is unlikely to appear spontaneously. No architect, no matter how talented, can transform a Big Ball of Mud into a modern microservices architecture without immense effort. Fortunately, projects can receive benefits without changing their entire architecture by building some flexibility points into the existing one.

Remove needless variability

One of the goals of Continuous Delivery is stability — building on known good parts. A common manifestation of this goal is the modern DevOps perspective on building immutable infrastructure.

While immutability may sound like the opposite of evolvability, quite the opposite is true. Software systems comprise thousands of moving parts, all interlocking in tight dependencies. Unfortunately, developers still struggle with unantecipated side effects of changes to one of those parts. By locking down the possibility of unantecipated change, we control more of the factors that make systems fragile. Developers strive to replace variables in code with constants to reduce vectors of change. DevOps introduced this concept to operations, making it more declarative.

Immutable infrastructure follows our advice to remove needless variables. Building software systems that evolve means controlling as many unknown factors as possible.

Make decisions reversible

Many DevOps practices exist to allow reversible decisions — decisions that need to be undone. For example, blue/green deployments, where operations have two identical (probably virtual) ecosystems — blue and green ones — common in DevOps.

Feature toggles are another common way developers make decisions reversible. By deploying changes underneath feature toggles, developers can release them to a small subset of users (called canary releasing) to vet the change. [...] Make sure you remove the outdated ones!

[...] Service routing — routing to a particular instance of a service based on request context — is another common method to canary release in microservices ecosystems.

Make as many decisions as possible reversible (withou over-engineering).

Prefer evolvable over predictable

Unknown unknowns are the nemesis of software systems. Many projects start with a list of known unknowns: things developers know they must learn about the domain and technology. However, projects also fall victim to unknown unknowns: things no one knew were going to crop up yet have appeared unexpectedly. This is why all Big Design Up Front software efforts suffer — architects cannot design for unkown unknowns.

While no architecture can survive the unknown, we know that dynamic equilibrium renders predictability useless in software. Instead, we prefer to build evolvability into software: if projects can easily incorporate changes, architects don't need a crystal ball.

Build anticorruption layers

Projects often need to couple themselves to libraries that provide incidental plumbing: message queues, search engines, and so on. The abstraction distraction antipattern describes the scenario where a project "wires" itself too much to an external library, either commercial or open source. Once it becomes time for developers to upgrade or switch the library, much of the application code utilizing the library has backed-in assumptions based on the previous library abstractions. Domain-driven design includes a safeguard against this phenomenon called an anticorruption layer.

Agile architects prize the last responsible moment principle when making decisions, which is used to counter the common hazard in projects of buying complexity too early.

[...] Most developers treat crufty old code as the only form of technical debt, but projects can inadvertently buy technical debt as well via premature complexity.

In the last responsible moment answer questions such as "Do I have to make this decision now?", "Is there a way to safely defer this decision without slowing any work?", and "What can I put in place now that will suffice but I can easily change later if needed?"

[...] Building an anticorruption layer encourages the architect to think about the semantics of what they need from the library, not the syntax of the particular API. But this is not an excuse to abstract all the things! Some development communities love preemptive layers of abstraction to a distracting degree but understanding suffers when you must call a Factory to get a proxy to a remote interface to a Thing. Fortunately, most modern languages and IDEs allow developers to be just in time when extracting interfaces. If a project finds themselves bound to an out-of-date library in need of change, the IDE can extract interface on behalf of the developer, making a Just In Time (JIT) anticorruption layer.

Controlling the coupling points in an application, especially to external resources, is one of the key responsibilities of an architect. Try to find the pragmatic time to add dependencies. As an architect, remember dependencies provide benefits but also impose constraints. Make sure the benefits outweigh the cost in updates, dependency management, and so on.

Architects must understand both benefits and tradeoffs and build engineering practices accordingly.

Using anticorruption layers encourages evolvability. While architects can't predict the future, we can at least lower the cost of change so that it doesn't impact us so negatively.

Build sacrificial architectures

[...] At an architectural level, developers struggle to anticipate radically changing requirements and characteristics. One way to learn enough to choose a correct architecture is build a proof of concept. Martin Fowler defines a sacrifical architecture as an architecture designed to be thrown away if the concept proves successful.

Many companies build a sacrificial architecture to achieve a minimum viable product to prove a market exists. While this is a good strategy, the team must eventually allocate time and resources to build a more robust architecture [...].

One other aspect of technical debt impacts many initially successful projects, elucidated again by Fred Brooks, when he refers to the second system syndrome — the tendency of small, elegant, and successful systems to evolve into giant, feature-laden monstrosities due to inflated expectations. Business people hate to throw away functioning code, so architecture tends toward always adding, never removing, or decomissioning.

Technical debt works effectively as a metaphor because it resonates with project experience, and represents faults in design, regardless of the driving forces behind them. Technical debt aggravates inappropriate coupling on projects — poor design frequently manifests as pathological coupling and other antipatterns that make restructuring code difficult. As developers restructure architecture, their first step should be to remove the historical design compromises that manifest as technical debt.

Mitigate external change

Most projects rely on a dizzying array of third-party components, applied via build tools. Developers like dependencies because they provide benefits, but many developers ignore the fact that they come with a cost as well. When relying on code from a third party, developers must create their own safeguards against unexpected occurrences: breaking changes, unannounced removal, and so on. Managing these external parts of projects is critical to creating evolutionary architecture.

We recommend that developers take a more proactive approach to dependency management. A good start on dependency management models external dependencies using a pull model. For example, set up an internal version-control repository to act as a third-party component store, and treat changes from the outside world as pull requests to that repository. If a beneficial change occurs, allow it into the ecosystem. However, if a core dependency disappears suddenly, reject that pull request as a destabilizing force.

Using a Continuous Delivery mindset, the third-party component repository utilizes its own deployment pipeline. When an update occurs, the deployment pipeline incorporates the change, then performs a build and smoke test on the affected applications. If successful, the change is allowed into the ecosystem. Thus, third-party dependencies use the same engineering practices and mechanisms of internal development, usefully blurring the lines across this often unimportant distinction between in-house written code and dependencies from third parties — at the end of the day, it's all code in a project.

Updating libraries versus frameworks

Because frameworks are a fundamental part of applications, teams must be aggressive about pursuing updates. Libraries generally form less brittle coupling points than frameworks do, allowing teams to be more casual about upgrades. One informal governance model treats framework updates as push updates and library updates as pull updates. When a fundamental framework (one whose afferent/efferent coupling numbers are above a certain threshold) updates, teams should apply the update as soon as the new version is stable and the team can allocate time for the change. Even though it will take time and effort, the time spent early is a fraction of the cost if the team perpetually procrastinates on the update.

Because most libraries provide utilitarian functionality, teams can afford to update them only when new desired functionality appears, using more of an "update when needed" model.

Prefer continuous delivery to snapshots

Continuous Delivery suggested a more nuanced way to think about dependencies, repeated here. Currently, developers only have static dependencies, linked via version numbers captured as metadata in a build file somewhere. However, this isn't sufficient for modern projects, which need a mechanism to indicate speculative updating. Thus, as the book suggests, developers should introduce two new designations for external dependencies: fluid and guarded. Fluid dependencies try to automatically update themselves to the next version, using mechanisms like deployment pipelines. For example, say that order fluidly relies on version 1.2 of framework. When framework updates itself to version 1.3, order tries to incorporate that change via its deployment pipeline, which is set up to rebuild the project anytime any part of it changes. If the deployment pipeline runs to completion, the fluid dependency between the components is updated. However, if something prevents successful completion — failed test, broken diamond dependency, or some other problem — the dependency is updated to a guarded reliance on framework1.2, which means the developer should try to determine and fix the problem, restoring the fluid dependency. If the component is truly incompatible, developers create a permanent static reference to the old version, eschewing future automatic updates.

None of the popular build tools support this level of functionality yet — developer must build this intelligence atop existing build tools. However, this model of dependencies works extremely well in evolutionary architectures, where cycle time is a critical foundational value, being proportional to many other key metrics.

Version services internally

[...] Developers use two common patterns to version endpoints, version numbering or internal resolution. For version numbering, developers create a new endpoint name, often including the version number, when a breaking change occurs. [...] The alternative is internal resolution, where callers never change the endpoint — instead, developers build logic into the endpoint to determine the context of the caller, returning the correct version. The advantage of retaining the name forever is less coupling to specific version numbers in calling applications.

In either case, severely limit the number of supported versions. The more versions, the more testing and other engineering burdens. Strive to support only two versions at a time, and only temporarily.

Neal Ford, Rebecca Parsons & Patrick Kua, "Building Evolvable Architectures", in Building Evolutionary Architectures: Support Constant Change, 107-119.

Sunday, January 26, 2020

Migrating architectures

Many companies end up migrating from one architectural style to another. One of the most common paths of migration is from monolith to some kind of service-based architecture, for reasons of the general domain-centric shift in architectural thinking [...]. Many architects are tempted by the highly evolutionary microservices architecture as a target for migration, but this is often quite difficult, primarily because of existing coupling.

When architects think of migrating architecture, they typically think of the coupling characteristics of classes and components, but ignore many other dimensions affected by evolution, such as data. Transactional coupling is as real as coupling between classes, and just as insidious to eliminate when restructuring architecture. These extra-class coupling points become a huge burden when trying to break the existing modules into too-small pieces.

Many senior developers build the same types of applications year after year, and become bored with the monotony. Most developers would rather write a framework than use a framework to create something useful: Meta-work is more interesting than work. Work is boring, mundane, and repetitive, whereas building new stuff is exciting.

Architects aren't immune to the "meta-work is more interesting than work" syndrome, which manifests in choosing inappropriate but buzz-worthy architectural styles like microservices.

Don't build an architecture just because it will be fun meta-work.

Migration steps

Many architects find themselves faced with the challenge of migrating an outdated monolithic application to a more modern service-based approach. Experienced architects realize that a host of coupling points exist in applications, and one of the first tasks when untangling a code base is understanding how things are joined. When decomposing a monolith, the architect must take coupling and cohesion into account to find the appropriate balance. For example, one of the most stringent constraints of the microservices architectural style is the insistance that the database reside inside the service's bounded context. When decomposing a monolith, even if it is possible to break the classes into small enough pieces, breaking the transactional contexts into similar pieces may present an unsurmountable hurdle.

Architects must understand why they want to perform this migration, and it must be a better reason than "it's the current trend". Splitting the architecture into domains, along with better team structure and operational isolation, allows for easier incremental change, one of the building blocks of evolutionary architecture, because the focus of work matches the physical work artifacts.

When decomposing a monolithic architecture, finding the correct service granularity is key. Creating large services alleviates problems like transactional contexts and orchestration, but does little to break the monolith into smaller pieces. Too-fine-grained components lead to too much orchestration, communication overhead, and interdependency between components.

Evolving module interactions

Migrating shared modules (including components) is another common challenge faced by developers. [...] Sometimes, the library may be split cleanly, preserving the separate functionality each module needs. [...] However, it's more likely the shared library won't split that easily. In that case, developers can extract the module into a shared library (such as a JAR, DLL, gem, or some other component mechanism) and use it from both locations.

Sharing is a form of coupling, which is highly discouraged in architectures like microservices. An alternative to sharing a library is replication.

In a distributed environment, developers may achieve the same kind of sharing using messaging or service invocation.

When developers have identified the correct service partitioning, the next step is separation of the business layers from the UI. Even in microservices architectures, the UIs often resolve back to a monolith — after all, developers must show a unified UI at some point. Thus, developers commonly separate the UIs early in the migration, creating a mapping proxy layer between UI components and the back-end services they call. Separating the UI also creates an anticorruption layer, insulating UI changes from architecture changes.

The next step is service discovery, allowing services to find and call one another. Eventually, the architecture will consist of services that must coordinate. By building the discovery mechanism early, developers can slowly migrate parts of the system that are ready to change. Developers often implement service discovery as a simple proxy layer: each component calls the proxy, which in turn maps to the specific implementation.

All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections.

Dave Wheeler and Kevlin Henney

Of course, the more levels of indirection developers add, the more confusing navigating the services becomes.

When migrating an application from a monolithic application architecture to a more service-based one, the architect must pay close attention to how modules are connected in the existing application. Naive partitioning introduces serious performance problems. The connection points in application become integration architecture connections, with the attendant latency, availability, and other concerns. Rather than tackle the entire migration at once, a more pragmatic approach is to gradually decompose the monolithic into services, looking at factors like transaction boundaries, structural coupling, and other inherent characteristics to create several restructuring iterations. At first, break the monolith into a few large "portions of the application" chunks, fix up the integration points, and rinse and repeat. Gradual migration is preferred in the microservices world.

Next, developers choose and detach the chosen service from the monolith, fixing any calling points. Fitness functions play a critical role here — developers should build fitness functions to make sure the newly introduced integration points don't change, and add consumer-driven contracts.

Neal Ford, Rebecca Parsons & Patrick Kua, "Building Evolvable Architectures", in Building Evolutionary Architectures: Support Constant Change, 100-103.

Refactoring and performance

[...] Most programmers, even experienced ones, are poor judges of how code actually performs. Many of our intuitions are broken by clever compilers, modern caching techniques, and the like. The performance of software usually depends on just a few parts of the code, and changes anywhere else don't make an appreciable difference.

But "mostly" isn't the same as "alwaysly". Sometimes a refactoring will have a significant performance implication. Even then, I usually go ahead and do it, because it's much easier to tune the performance of well-factored code. If I introduce a significant performance issue during refactoring, I spend time on performance tuning afterwards. It may be that this leads to reversing some of the refactoring I did earlier — but most of the time, due to the refactoring, I can apply a more effective performance-tuning enhancement instead. I end up with code that's both clearer and faster.

So, my overall advice on performance with refactoring is: Most of the time you should ignore it. If your refactoring introduces performance slow-downs, finish refactoring first and do performance tuning afterwards.

Martin Fowler, "Refactoring: A First Example", in Refactoring: Improving the Design of Existing Code (2nd Edition), 20.

Saturday, January 25, 2020

The first step in refactoring

Bfore you start refactoring, make sure you have a solid suite of tests. These tests must be self-checking.

Martin Fowler, "Refactoring: A First Example", in Refactoring: Improving the Design of Existing Code (2nd Edition), 5.

The purpose of refactoring

Given that the program works, isn't any statement about its structure merely an aesthetic judgment, a dislike of "ugly" code? After all, the compiler doesn't care whether the code is ugly or clean. But when I change the system, there is a human involved, and humans do care. A poorly designed system is hard to change — because it is difficult to figure out what to change and how these changes will interact with the existing code to get the behavior I want. And if it is hard to figure out what to change, there is a good chance that I will make mistakes and introduce bugs.

Thus, if I'm faced with modifying a program with hundreds of lines of code, I'd rather it be structured into a set of functions and other program elements that allow me to understand more easily what the program is doing. If the program lacks structure, it's usually easier for me to add structure to the program first, and then make the change I need.

Let me stress that it's these changes that drive the need to perform refactoring. If the code works and doesn't ever need to change, it's perfectly fine to leave it alone. It would be nice to improve it, but unless someone needs to understand it, it isn't causing any real harm. Yet as soon as someone does need to understand how that code works, and struggles to follow it, then you have to do something about it.

Martin Fowler, "Refactoring: A First Example", in Refactoring: Improving the Design of Existing Code (2nd Edition), 4-5.

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

Martin Fowler, "Refactoring: A First Example", in Refactoring: Improving the Design of Existing Code (2nd Edition), 10.

Saturday, January 18, 2020

Refactoring versus restructuring

Developers sometimes co-opt terms that sound cool and make them into broader synonyms, as is the case for refactoring. As defined by Martin Fowler, refactoring is the process of restructuring existing computer code without changing its external behavior. For many developers, refactoring has become synonymous with change, but there are key differences.

It is exceedingly rare that a team refactors an architecture: rather, they restructure it, making substantive changes to both structure and behavior. Architecture patterns exist in part to make certain architectural characteristics primary in an application. Switching patterns entails switching priorities, which isn't refactoring. For example, architects might choose an EDA for scalability. If the team siwtches to a different architectural pattern, it likely won't support the same level of scalability.

Neal Ford, Rebecca Parsons & Patrick Kua, "Building Evolvable Architectures", in Building Evolutionary Architectures: Support Constant Change, 99.

Retrofitting existing architectures

Adding evolvability to existing architectures depends on three factors:

Appropriate coupling and cohesion

Beyond the technical aspects of coupling, architects must also consider and defend the functional cohesion of the components of their system. For example, some business problems are more coupled than others, such as in the case of heavily transactional systems. Trying to build an extremely decoupled architecture that is counter to the problem is unproductive.

Understand the business problem before choosing an architecture.

While this advice seems obvious, we constantly see teams that have chosen the shiniest new architectural pattern rather than the most appropriate one suffer. Part of choosing an architecture lies in understanding where the problem and physical architecture come together.

Engineering practices

While Continuous Delivery practices don't guarantee evolutionary architecture, it is almost impossible without them.

The biggest single common impediment to building evolutionary architecture is intractable operations. If developers cannot easily deploy changes, all parts of the feedback cycle are hampered.

Fitness functions

We encourage architects to start thinking of all kinds of architectural verification mechanisms as fitness functions, including things they have previously considered ad hocly. For example, many architectures have a service-level agreement around scalability and corresponding tests. They also have rules around security requirements, with accompanying verification mechanisms. Architects often think of these as separate categories, but both intents are the same: verify some feature of the architecture. By thinking of all architectural verification as fitness functions, there is more consistency when automation and other beneficial synergistic interactions are defined.

Neal Ford, Rebecca Parsons & Patrick Kua, "Building Evolvable Architectures", in Building Evolutionary Architectures: Support Constant Change, 97-99.

Building evolvable architectures

Many of the concepts we discussed aren't new ideas, but rather viewed through a new lens. For example, testing has existed for years, but not with the fitness function emphasis on architectural verification. Continuous Delivery defined the idea of deployment pipelines. Evolutionary architecture shows architects the real utility of that capability.

Many organizations pursue Continuous Delivery practices as a way to increase engineering efficiency for software development, a worthy goal in itself. However, we're taking the next step, using those capabilities to create something more sophisticated — architectures that evolve with the real world.

Mechanics

Architects can operationalize these techniques for building an evolutionary architecture in three steps:

  1. Identify dimensions affected by evolution
  2. Define fitness function(s) for each dimension
  3. Use deployment pipelines to automate fitness functions

[...] Software suffers from the unknow unknows problem: developers cannot anticipate everything. [...] While some fitness functions will naturally come to light at the beginning of a project, many won't reveal themselves until an architectural stress point appears. Architects must vigilantly watch for situations where nonfunctional requirements break and retrofit the architecture with fitness functions to prevent future problems.

Neal Ford, Rebecca Parsons & Patrick Kua, "Building Evolvable Architectures", in Building Evolutionary Architectures: Support Constant Change, 95-96.

Wednesday, January 8, 2020

Logs em formato JSON com Python

Python possui um módulo de logging bastante robusto já embutido em sua biblioteca padrão. Esse módulo disponibiliza muita coisa pronta para que qualquer pessoa comece a incluir logs em sua aplicação sem grande esforço. Não obstante, o módulo é também altamente configurável, permitindo personalizar a estrutura de handling dos logs, o formato das mensagens, entre outras coisas.

Em algumas situações, pode ser interessante que a mensagem de log gerada seja um JSON bem formatado, contendo informações sobre o fluxo da aplicação em formato chave-valor. Isto é, em vez de emitir logs esparsos, que teriam que ser agregados para dar visão geral do fluxo, poderíamos ter um único log em formato JSON impresso ao final da execução de determinado fluxo (independentemente desse fluxo ter terminado de forma bem sucedida ou não). Essa abordagem facilita não somente a busca por mensagens específicas (usando ferramentas como o CloudWatch Logs Insights) mas também o próprio processo de depuração de problemas, concentrando as informações necessárias num formato bem estruturado.

Em Python, podemos gerar strings JSON a partir de dicionários. Usando as cláusulas try/except/finally, consequimos também garantir a impressão dos logs qualquer que seja o resultado da execução do fluxo contido no bloco de tratamento de exceções. A maior dificuldade é repetir todo esse boilerplate nos pontos em que é necessário aplicar o log JSON. Podemos então lançar mão de mais um recurso da linguagem a fim de evitar essa repetição: decoradores. O Gist a seguir mostra uma maneira de conectar todas essas ideias para construir um decorador que, ao ser aplicado a um método/função, permite construir facilmente um log JSON que será impresso ao final da execução desse método/função, ainda que um erro aconteça durante a execução.

Wednesday, January 1, 2020

Evolutionary database design

In Chapters 1 and 4, we discussed the architectural quantum boundary concept definition: the smallest architectural deployable unit, which differs from traditional thinking about cohesion by encompassing dependent components like databases. The binding created by databases is more imposing than traditional coupling because of transactional boundaries, which often define how business processes work. Architects sometimes err in trying to build an architecture with a smaller level of granularity than is natural for the business. For example, microservices architectures aren't particularly well suited for heavily transactional systems because the goal service quantum is so small. Service-based architectures tend to work better because of less strict quantum size requirements.

Architects must consider all the coupling characteristics of their application: classes, package/namespace, library and framework, data schemas, and transactional contexts. Ignoring any of these dimensions (or their interactions) creates problems when trying to evolve an architecture. In physics, the strong nuclear force that binds atoms togheter is one of the strongest forces yet identified. Transactional contexts act like a strong nuclear force for architecture quanta.

While systems often cannot avoid transactions, architects should try to limit transactional contexts as much as possible because they form a tight coupling knot, hampering the ability to change components or services without affecting others. More importantly, architects should take aspects like transactional boundaries into account when thinking about architectural changes.

As discussed in Chapter 8, when migrating a monolithic architectural style to a more granular one, start with a small number of large services first. When building a greenfield microservices architecture, developers should be diligent about restricting the size of service and data contexts. However, don't take the name microservices too literally — each service doesn't have to be small, but rather capture a useful bounded context.

When restructuring an existing database schema, it is often difficult to achieve appropriate granularity. Many enterprise DBAs spend decades stitching a database schema togheter and have no interest in performing the reverse operation. Often, the necessary transactional contexts to support the business define the smallest granularity developers can make into services. While architects may aspire to create a smaller level of granularity, their efforts slip into inappropriate coupling if it creates a mismatch with data concerns.

Age and quality of data

Before trying to build an evolutionary architecture, make sure developers can evolve the data as well, both in terms of schema and quality. Poor structure requires refactoring, and DBAs should perform whatever actions are necessary to baseline the quality of data. We prefer fixing these problems early rather than building elaborate, ongoing mechanisms to handle these problems in perpetuity.

Legacy schemas and data have value, but they also represent a tax on the ability to evolve. Architects, DBAs, and business representatives need to have frank conversations about what represents value to the organization — keeping legacy data forever or the ability to make evolutionary change. Look at the data that has true value and preserve it, and make the the older data available for reference but out of the mainstream of evolutionary development.

Neal Ford, Rebecca Parsons & Patrick Kua, "Evolutionary Data", in Building Evolutionary Architectures: Support Constant Change, 91-93.