Working on small, monolithic applications with a small team is a joy. You can understand everything that happens in the system and what everyone else is working on. That also means you can quickly make and deploy meaningful changes. The boundaries of the system and the team are manageable. Beyond a certain scale of scope, […]
Working on small, monolithic applications with a small team is a joy. You can understand everything that happens in the system and what everyone else is working on. That also means you can quickly make and deploy meaningful changes. The boundaries of the system and the team are manageable. Beyond a certain scale of scope, usage, and/or team size, though, that monolith you loved will become a burden.
With some preparation and foresight, you can get ready for that growth. The key is knowing what boundaries to create and when to create them.
Creating boundaries within a system means creating well-defined borders that can control scope and complexity. This is helpful to the people involved and provides more technical control options. Here are a few reasons I’ve found it helpful to compartmentalize parts of a system.
Boundaries limit the amount of complexity that can live inside the boundaries — inside the box. That’s good because we all have limits for how much complexity we can keep in our brains at once. When the complexity inside the box is lower than those limits, it’s easier to work in that box.
Having many smaller boxes also creates more opportunities for autonomy and evolution. As long as the behavior at the boundaries meets expectations, you can build one subsystem in a more functional programming style, while another can be more object-oriented. One can use TypeScript and another can use Python or Rust.
Additionally, several smaller systems with well-defined boundaries are easier to work on in parallel. Boundaries in code and infrastructure make it less likely that team members will step on each other’s toes.
With a monolith, deployment is often an all-or-nothing affair. The bigger the monolith the more of a problem that becomes. The right level of separation lets you deploy changes (especially hotfixes) to only the affected parts of your infrastructure. That limits downtime and impact.
When you separate parts of the system, you have more options for which parts of the infrastructure you scale and how you control load and throughput. For example, if you have one data storer for all reads and writes, you may find yourself with users performing actions that they expect to run long (reports). Other users will be performing actions that they expect to return quickly (mutations), and both sets of users are competing for resources.
It would be difficult to meet expectations consistently for both sets of users with scaling alone. But, if the data stores are separate, then scaling can be set to balance cost and performance for each use case independently.
When you decide that creating boundaries would be helpful, figuring out where to find or create those boundaries is the next step. Some areas I look at for boundary opportunities include:
Some tools and practices work best when employed from the beginning of a project, while some you can bring in later as needed. It’s tempting to avoid over-complicating your project. But making smart investments at the beginning helps to avoid complicated rework later. Here are techniques that have worked to create separation for me or that I have on my radar for future projects.
When: From the start
What: A mechanism that lets you publish and subscribe to domain-level events within a single process
Why: Keep the code focused and decoupled from secondary effects
Example: A customer domain entity repository publishes customer changed events, and a customer changed event handler uses the customer orders repository to make necessary updates to those entities.
When: Segregation of scaling, responsible team, or other factors makes the separation of processes worthwhile
What: Use messaging infrastructure to decouple components into separate processes
Why: Keep processes focused and constrain the complexity of related code. Create opportunities for different teams to be responsible for different parts of the system. Avoid placing undesirable constraints on an entire system that only needs to apply to one part of it.
Example: Generating Excel files takes longer than allowed by API Gateway’s 29s limit. That means the request to create the file gets placed in a queue and handled by a separate Lambda that’s not bound by the 29s limit. Or, an update to an entity in one part of the system needs to trigger a report update in another part of the system. In that case, the information is placed in a message stream and handled by a special report updater Lambda.
When: From the start
What: Organize a project into folders that align with the boundaries you want to see in the application
Why: Organizing based on desired boundaries can help make it clear when those boundaries are ignored or broken. Automated tools may be able to help enforce those boundaries, depending on language and tools. For instance, Lint tools for TypeScript can enforce import restrictions.
Example: Rather than creating directories based on the horizontal slices of an application’s architecture (e.g., records, domain, logic), create directories based on vertical slices of functionality (e.g., pages/report, graphql-api/report, core/product, etc.).
When: From the start
What: Like separation above, but a more formal and well-tooled way to separate and independently version and publish modules that live together in a single Git repository. I haven’t used Lerna yet, but it’s high on the list for my next project.
Why: Create a more formal separation between code modules in a monorepo, without sacrificing most of the convenience of the monorepo.
Example: See the Lerna documentation
In the course of a project, you will run into situations where having boundaries in the right places will make your life much easier. Building a foundation with sensible boundaries and tools for creating new boundaries when the need arises is a good start.
Creating the right boundaries is a constant exercise in balance. As future growth becomes clearer, you’ll have more information to help you decide on potential boundaries and separations. Create low-effort, low-impact boundaries (e.g., in-process event pub-sub system) more aggressively. On the other hand, think carefully about where it makes sense to invest in higher-cost, higher-impact separations (e.g., using a Kinesis stream or SQS queue to separate processes at the infrastructure level) and take those on selectively.
The post Prepare Your Software for Growth: Create Boundaries appeared first on Atomic Spin.
Source: Atomic Object