Several decisions we’ve made earlier became powerful enablers for our first pass at Scalability.
Data reads take time, reading less than everything is always a good idea. Implementing Repositories as queryable allowed us to switch to paged, ordered and generally highly selective views.
Unit of Work
Effectively building small projections for the UI from large datasets was possible due to request-scoped Unit of Work implementation, enabling underlying ORM to read data across multiple Repositories.
Root Aggregate, defined as a cluster of associated objects treated as a unit for the purpose of data changes. By implementing Repositories one per aggregate, we isolated data affected by transactions and avoided deadlocks. With a bit of per-ID synchronization we were able to avoid optimistic concurrency exceptions as well.
By integrating message passing early, we were able to move all the writes to backend services and avoid distributed locks in exchange for eventual consistency. Moving the backend services to another machine after this was trivial.
And that’s how by scaling out we were able to get that x100 performance gain, on the cheap. We’ll do it again a few months later by batching, denormalizing the data and embracing Event-Driven SOA.