Skip to the content.

Explicit Architecture - right way to track contract dependencies What our services can do depends on their dependencies. Dependencies may be different, have different life cycle and value. However, there is one common methodological aspect about dependencies: dependencies may be declared explicitly or may occur implicitly. I will explain why, from my experience, explicit dependency is always better and implicit dependency looks like a quick solution but causes problems. Importance of “Explicit” word in architecture is on different levels. “Explicit” is related to “Dependencies”. I just will show few aspects when Explicit helps us to track dependencies.

  1. Strongly typed languages allow us to declare objects with a contract structure. For example, TypeScript is better than JavaScript. We may know what to expect from an object.
  2. OOP encapsulation is about sharing only explicitly specified contracts: allowing access only to state that we explicitly allow access to and calling only methods that we explicitly allow to call.
  3. One of the core principles of Service Oriented Architecture is “Contract First”. We explicitly say what we publish as a contract and nothing else. A contract is something that has a life cycle and consequences.
  4. Database table schema is an explicit contract. NoSQL doesn’t mean no schema; it means that the NoSQL engine doesn’t enforce schema, but explicit schema must be on the logic level. You may see Martin Fowler’s NoSQL book. Too many stories exist about teams who were putting data into NoSQL without any responsibility for the schema.
  5. Functional languages make declarations immutable by default because if you want mutability you have to ask for it explicitly. It helps avoid situations where someone messes up structures from a parallel thread.
  6. Dependency Injection is about explicit tracking of dependencies. Dependencies in a composition graph should form a direct graph without loops or any kind of cross-coupling or transitive coupling.
  7. There are contract refactoring practices, e.g. Database Refactoring Patterns, and they are complex processes when a contract is unified and deprecated

There may be many more examples, but let’s talk about cases when implicit dependencies occur:

  1. Separation of concerns: Domain objects should be separated from IO contracts (DataAccessObjects or DataTransfer Objects). When we don’t do such a separation, any change to domain objects breaks IO contracts. It does not become important until a small pilot starts growing or changing. It is a healthy practice to be able to change everything that is not marked as a contract. If it compiles then it works. If a property rename in a domain object causes an outage because it was unexpectedly sent to Kusto and someone developed logic based on that property name, then the whole project philosophy becomes “don’t touch it if it works”, but the reality is that “don’t touch this card because it is what keeps this house of cards standing”.
  2. I don’t like automappers. Modern languages, especially C#9 and F#, support record types that allow mapping in a very short manner. Using automappers is like putting a piece of vanilla JavaScript into a strongly typed .NET solution. When we map to an IO contract we make it explicitly because in that moment we declare a contract between our process and the out-of-process world.
  3. Violation of SOA principles. There is no microservice architecture without SOA principles. The distributed monolith antipattern is what happens when people go to microservices while ignoring SOA principles. It is an incredibly common issue in many companies. And eventually this story becomes a drama when someone else comes to the project and understands that it is a distributed monolith and has to rejoin or resplit services first before being able to meet the new growth.

There is a classic story in project management books about the V2 crisis that happens when V1 was released successfully and customers want V2 released with even faster tempo. That’s why architects invented SOLID and SOA principles. SOLID allows reusability on the OOP level. SOLID allows us not to drop classes on V2. SOA allows reusability on the service level. SOA allows us to design services that will be sold in a few years in the future. Both principles are mirrored and have a common goal. Please make contracts explicit, spend a minute to map them, and save days during contract upgrades. Making implicit contracts is irresponsible toward those who will support those contracts because they will have to recursively scan who and how may use anything as a contract. Everything that went out of the process may become an implicit contract for many subsystems. Implicit contracts cause chaos with many “butterfly effects”.

Having an “Explicit Contracts Culture” (personal, team, and corporate) is very important. The absence of a corporate “Explicit Contracts Culture” is the end of the business. Microsoft has a very strong culture of contracts because it is the ground of relationships with customers. I know that the COSINE team is still making some patching to Windows XP customers, and that is a big challenge to evolve new and support old. I think that not all teams articulate a contracts culture. On an individual level, a developer may skip making explicit contracts to prototype quickly, but I want all classes that are contracts to be marked in some way before release. If a class is a contract for both DocumentDb and Kosmos there must be a note about this. Otherwise, when I see the class I should see that it has two responsibilities, and if I want to change only the Kusto contract I will split one data model into two and modify only one (e.g. the Kusto-related one). Working in projects where a class has thousands of usages and where it is necessary to research all of them before making a decision about its implicit-contract usage is a long and hard-to-track process.

That’s why personally I always create separate data models for each individual persistence source. I needed to persist one entity to Redis and Kosmos, so I created a common canonical model, a Redis data model, and a Kusto data model, then remapped and serialized them individually. Now I may evolve my canonical common model as I want because only it is compilable, but Redis and Cosmos DB models are contracts.