How We Design Our Applications
We organize our applications around business domains (e.g., trading) into domain oriented services, instead of around entities (e.g., accounts) into entity oriented services because we want to center the business logic, not the undifferentiated heavy lifting required to provide differently-motivated consumers with domain-specific data access patterns to a shared resource.
We split domains into new applications and business units instead of resorting to modules-of-modules to manage complexity within a single app because app domains that large are too big for a human to fit in their head at once anyway, and ownership gets fuzzy at the module boundaries.
Given we’re not absolutely sure what the public surface area and eventual supporting team shape should be, we prefer to build new apps when we have a fully staffed squad motivated by a business concern to drive it because contract change is easier before extraction, and not every idea has the legs we hope it will.
We do still build new apps without a fully staffed squad when the following criteria are met:
- The new app has a clearly owned data model not requiring extensive synchronization with the original app, or it has very little/no data model of its own and operates semi-statelessly
- The new app has a clear, business-motivated domain that makes it clear what functionality belongs in the new app
- The contract between the new app and the old app is not expected to evolve significantly as we experiment and learn (i.e. it is a mature/well-understood domain) and there is an inherent benefit in creating a separate app to own this business domain
Given no legacy need and outside of ETL scenarios, our application domains solely own their databases and filestores (s3 buckets), and no application is allowed to connect to another application’s database or filestore because doing so implicitly couples the applications at the data layer, eliminating the owning team’s ability to iterate on data model without coordinating with a third-party team.
Given no exceptional scale or legacy need, we use a single database per application and no persistent cache store (e.g., Redis) because additional datastores break transactional semantics (or require a distributed transaction manager), forcing us to take on more complexity to deliver features and operate the application safely.
Given no legacy need, we use a single filestore (s3 bucket) per application because they are already infinitely scalable and we can solve for data sensitivity of subsets with finer-grained tools.
Given we aren’t forced to by IaaS provider design decisions, we use FaaS (AWS Lambda) only for standalone units of functionality because if we can’t humanely represent the entire business domain within a single function, we’ve signed up for a greater testing, coordination, and operational burden per feature to achieve the same confidence in our services, and PaaS is already serverless.
No app owns data to the exclusion of others, but shared facts must flow asynchronously, unidirectionally through the ecosystem. Any app can store a local representation of a fact that it needs for filtering, aggregation, liveness, or performance reasons, because only with ownership of our own facts can we reap the full benefits of a transactional, relational datastore. Note that this data is not a cache just as our potentially out-of-date info originally entered by the customer is not a cache. It is never evicted, only updated or added-to when new facts flow through the system.