Blog
Reading nopCommerce as a System: A Monolith's Lessons for System Design
We tend to talk about writing code and designing systems as if they were separate skills, and to treat a monolith as the opposite of good system design. Both framings are a little off. A well-architected monolith like modern nopCommerce is a system. It has internal boundaries, layered dependencies, a caching strategy, an event mechanism, and background work. The reasoning you do to keep it clean is the same reasoning behind system design.
The honest difference is where the boundaries sit. In a monolith they are in-process, a method call away. In a distributed system they are over the network, an API call away. That one change removes a whole class of failure modes and adds another, but the architectural thinking on either side is the same. This post maps nopCommerce onto system-design ideas, goes deep on caching, and is careful about where the analogy holds and where it breaks.
Two altitudes, one engineer
First, the distinction the post is built on. Writing code and designing a system are different altitudes of the same work.
Read it as one continuum rather than two jobs. The habits that make you good at the small end, drawing boundaries, watching coupling, naming what depends on what, are the same habits that hold up at the large end.
nopCommerce is a modular monolith, not a big ball of mud
The reason nopCommerce is a good lens is that it is not a tangle. Its architecture is close to onion architecture: layers with a strict dependency rule where everything points inward, and the core depends on nothing.
Concretely: Nop.Core holds the domain entities, the caching and event abstractions, and infrastructure, and it references no other project. Nop.Data holds the data access (the IRepository<TEntity> abstraction, LinqToDB, and FluentMigrator migrations) and may depend on Core. Nop.Services holds the business logic and depends on Data and Core. The web and plugin layer sits on the outside. Dependencies only ever point inward.
That dependency rule is not academic. It is the same decision you make at system scale when you decide which service is allowed to call which, and which way the arrows are allowed to point. Getting it right in a monolith is the training-wheels version of getting it right across services.
The parallels
Once you see nopCommerce as a set of in-process boundaries, most of its infrastructure maps directly onto a system-design idea.
A few worth drawing out:
- Events. nopCommerce has an
EventPublisherandIConsumer<T>handlers. Publish an event, and any number of consumers react, with no direct reference between them. That is in-process publish and subscribe, the same decoupling a message bus gives you between services, minus the network. - Background work.
IScheduleTaskruns recurring jobs on a schedule inside the app. At system scale that becomes a worker pool pulling from a queue, but the design question, what runs inline versus out of band, is identical. - The data boundary. Services talk to
IRepository<TEntity>and a data-provider abstraction, not to a specific database directly. That indirection is why nop can target different databases, and it is the same seam you would put in front of a data store you might later swap or shard. - Scaling out. nopCommerce runs behind a load balancer as multiple web instances sharing one database and a distributed cache. The moment you do that, you are doing system design, and caching is where it gets real.
Caching, in depth
Caching is the richest parallel, so it is worth going slow. nopCommerce's caching is a textbook cache-aside setup with prefix-based invalidation, and both halves map straight onto system design.
Walking through it:
Reads are cache-aside. The core abstraction is
IStaticCacheManager. A caller does not check the cache and then decide what to do. It callsGet(cacheKey, acquire), and the manager returns the cached value if it is there, or runs theacquirefunction on a miss, stores the result under the key with a time-to-live, and returns it. The calling code never branches on hit or miss. Cache-aside is the single most common caching pattern in system design, and this is it, exactly.Keys are namespaced on purpose. Every cached object gets a structured key: a name plus identifiers, like a product key that includes the product id, grouped under prefixes such as a products prefix. That naming is not tidiness for its own sake. It is what makes the next part possible.
Invalidation is by prefix. When an entity is inserted, updated, or deleted, nopCommerce calls
RemoveByPrefix(prefix)to evict every key under that namespace, rather than trying to surgically remove one key it might miss. Blunt, but correct: after a product changes, drop everything under the products prefix and let the next read repopulate. Cache invalidation is famously one of the hard problems, and prefix eviction is a pragmatic, real answer to it.There is a short-lived per-request layer. Within a single request, repeated lookups do not keep hitting the backing store. This is request-scoped memoization sitting in front of the shared cache.
Memory versus distributed is the system-design fork. On one server,
MemoryCacheManagerkeeps everything in process, fast, no serialization. The moment you run more than one web node behind a load balancer, that breaks: node A does not know node B just evicted a key, so node A serves stale data. The fix is a distributed cache,DistributedCacheManagerbacked by Redis, so a single eviction is visible to every node. Deciding between a fast local cache and a consistent shared one, and accepting the serialization cost and consistency questions that come with the shared one, is a pure system-design tradeoff, and nopCommerce makes you make it the instant you scale out.TTL is the backstop. Every key has an expiry, so even if some invalidation path is missed, stale data has a shelf life. Expiration policy as a safety net is the same thinking you apply to any cache.
If you understand this one subsystem well, you understand cache-aside, key design, invalidation strategy, local versus distributed caching, request-scoped memoization, and expiration. That is most of a system-design conversation about caching, learned from one monolith.
Where the analogy holds, and where it breaks
Now the honest part, because overclaiming here would undo the point. A monolith teaches you a lot, but it is not a distributed system, and pretending otherwise fails the moment someone asks a pointed question.
What transfers cleanly: reasoning about boundaries and coupling, deciding what is async versus inline, caching strategy, data-access design, and event-driven decoupling. A monolith exercises all of these.
What a monolith does not teach you, and what you should not claim it does: network partitions and partial failure, retries and idempotency across the wire, distributed transactions and eventual consistency between services, independent deployment and versioning, and scaling stateless services horizontally as separate units. Those only show up once the boundary is a network. The value of having built and reasoned about a clean monolith is that you arrive at those problems already fluent in the part that does transfer, so the new material is the network, not the fundamentals.
Modularity is the groundwork
There is one more reason a well-built monolith is good system-design practice, not a detour from it. Its module boundaries are exactly where you would cut services if you ever needed to.
The mainstream advice is to start with a modular monolith and extract services only when a real pressure, scaling, team boundaries, deploy independence, forces it, and to extract along the seams you already drew. That means the work of keeping a monolith's modules clean is not separate from system design. It is the first move in it. nopCommerce, with its layers, plugins, and events, is a working example of what those seams look like.
Bottom line
A monolith is not the opposite of system design. A well-architected one is a system you can hold in your head, and reasoning about its boundaries, its caching, its events, and its data access is the same reasoning you apply when those boundaries become a network. nopCommerce is a good place to have practiced it, because it is genuinely layered and its caching, in particular, is a real strategy rather than an afterthought. The honest framing is not that a monolith makes you a distributed-systems expert. It is that it makes the fundamentals second nature, so the distributed part is the only part left to learn.
If you run a nopCommerce store and want someone who reasons about its architecture, not just its features, that is the work I do. You can see examples of my nopCommerce work, or get a quote.