<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nsikan Patrick Adaowo</title>
    <description>The latest articles on DEV Community by Nsikan Patrick Adaowo (@nsikanadaowo).</description>
    <link>https://dev.to/nsikanadaowo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3061717%2F705a3df2-2eb9-4d99-8071-08fcc00c9b4a.png</url>
      <title>DEV Community: Nsikan Patrick Adaowo</title>
      <link>https://dev.to/nsikanadaowo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nsikanadaowo"/>
    <language>en</language>
    <item>
      <title>N+1 Queries: Your ORM Is Lying to You and Your Database Is Paying the Bill</title>
      <dc:creator>Nsikan Patrick Adaowo</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:02:39 +0000</pubDate>
      <link>https://dev.to/nsikanadaowo/n1-queries-your-orm-is-lying-to-you-and-your-database-is-paying-the-bill-15in</link>
      <guid>https://dev.to/nsikanadaowo/n1-queries-your-orm-is-lying-to-you-and-your-database-is-paying-the-bill-15in</guid>
      <description>&lt;p&gt;Your ORM is supposed to make database access easier, so why is your application making hundreds of database queries to return a single response?&lt;/p&gt;

&lt;p&gt;You define your models, establish your relationships, and let the ORM handle the details. No complicated SQL. No messy joins. Just clean, readable application code.&lt;/p&gt;

&lt;p&gt;Except there’s a problem.&lt;/p&gt;

&lt;p&gt;That innocent-looking loop might be querying the database once for every record it retrieves. Your code stays simple, but your database pays for every additional round trip.&lt;/p&gt;

&lt;p&gt;And adding an index might make those queries faster without fixing the underlying problem.&lt;/p&gt;

&lt;p&gt;The issue isn’t always how efficiently you’re querying your database. Sometimes, it’s how many times you’re querying it in the first place.&lt;/p&gt;

&lt;p&gt;This is the N+1 query problem. And it’s particularly frustrating because the code that introduces it can look perfectly reasonable.&lt;/p&gt;

&lt;p&gt;An ORM promises to make database interactions easier to work with. It gives you models, relationships, and convenient methods for retrieving data. But convenient database access is not the same thing as efficient database access.&lt;/p&gt;

&lt;p&gt;Your application code might look simple while the database handles a workload that was never necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Exactly Is an N+1 Query Problem?
&lt;/h2&gt;

&lt;p&gt;Imagine you’re building a SaaS application with users and their projects.&lt;br&gt;
You need an endpoint that returns a list of users alongside the projects each user owns.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The data model is straightforward:&lt;/li&gt;
&lt;li&gt;A User can own many Project records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each Project belongs to one User.&lt;/p&gt;

&lt;p&gt;In a relational database, this is a familiar one-to-many relationship.&lt;/p&gt;

&lt;p&gt;The naive implementation might look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqilrwwiuot8zv6f1nqy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqilrwwiuot8zv6f1nqy.png" alt=" " width="800" height="756"&gt;&lt;/a&gt;&lt;br&gt;
Nothing about this code looks particularly alarming.&lt;/p&gt;

&lt;p&gt;You retrieve the users, loop through them, and fetch each user’s projects. The code is readable, the relationship is clear, and the response has the shape you need.&lt;/p&gt;

&lt;p&gt;But look at what happens at the database level.&lt;/p&gt;

&lt;p&gt;The first query retrieves the users:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SELECT *&lt;br&gt;
FROM "User";&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then, for every user returned, the application executes another query:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SELECT *&lt;br&gt;
FROM "Project"&lt;br&gt;
WHERE "userId" = $1;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If the first query returns 50 users, the database receives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 query to retrieve the users.&lt;/li&gt;
&lt;li&gt;50 additional queries to retrieve their projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s 51 queries to construct one response.&lt;/p&gt;

&lt;p&gt;If the endpoint returns 500 users, it can execute 501 queries.&lt;/p&gt;

&lt;p&gt;This is why the problem is called N+1: one initial query, followed by N additional queries.&lt;/p&gt;

&lt;p&gt;The exact number depends on the data and the code path, but the underlying issue remains the same: the number of database queries grows with the number of records returned by the initial query.&lt;br&gt;
And that growth can happen without any obvious change to the service’s complexity or readability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why N+1 Queries Are More Expensive Than They Look
&lt;/h2&gt;

&lt;p&gt;A common mistake is to assume that an individual query is cheap, so executing many of them must also be cheap.&lt;br&gt;
That assumption ignores the work surrounding each query.&lt;/p&gt;

&lt;p&gt;A database query isn’t just the time PostgreSQL spends executing SQL. The application and database must also coordinate the operation.&lt;/p&gt;

&lt;p&gt;Depending on the driver, connection pool, network, and execution path, each query may involve:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sending a request from the application to the database.&lt;/li&gt;
&lt;li&gt;Parsing, planning, or executing the statement as applicable.&lt;/li&gt;
&lt;li&gt;Reading or modifying the relevant data.&lt;/li&gt;
&lt;li&gt;Returning the result to the application.&lt;/li&gt;
&lt;li&gt;Converting the result into application-level objects.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some of these costs can be reduced through prepared statements, connection reuse, or other optimizations. But repeating a query still creates work.&lt;br&gt;
Consider a simplified example.&lt;/p&gt;

&lt;p&gt;Suppose each additional query takes an average of 5 milliseconds from the application’s perspective, including the relevant round-trip and processing overhead.&lt;/p&gt;

&lt;p&gt;With 100 users, the endpoint executes 101 queries.&lt;/p&gt;

&lt;p&gt;If those additional queries run sequentially, the repeated database operations alone could account for roughly 500 milliseconds, before adding the initial query, serialization, and other application work.&lt;/p&gt;

&lt;p&gt;That number is illustrative, not a performance benchmark. Actual latency depends on the environment and workload.&lt;/p&gt;

&lt;p&gt;The important point is that a query taking only a few milliseconds can become a serious problem when repeated hundreds of times.&lt;/p&gt;

&lt;p&gt;And sequential execution isn’t the only concern.&lt;/p&gt;

&lt;h3&gt;
  
  
  The database is doing unnecessary work
&lt;/h3&gt;

&lt;p&gt;Every additional query consumes some combination of database CPU, connection time, query execution resources, memory, and I/O.&lt;/p&gt;

&lt;p&gt;Even when an index makes each lookup efficient, the database still has to process every request.&lt;br&gt;
An index can make a query faster. It cannot eliminate the need to execute that query 100 times.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your connection pool becomes a bottleneck
&lt;/h3&gt;

&lt;p&gt;Database connections are finite resources.&lt;/p&gt;

&lt;p&gt;When an application generates large numbers of queries, those queries must compete for available connections and database capacity.&lt;/p&gt;

&lt;p&gt;This becomes particularly problematic when multiple requests are doing the same thing simultaneously.&lt;/p&gt;

&lt;p&gt;One endpoint might execute 101 queries. Fifty concurrent requests could generate thousands of database operations in a short period.&lt;/p&gt;

&lt;p&gt;The result can be increased connection wait time, growing latency, and reduced throughput.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your tail latency gets worse
&lt;/h3&gt;

&lt;p&gt;Users don’t experience your endpoint as a collection of individually fast queries. They experience the total time required to receive a response.&lt;/p&gt;

&lt;p&gt;A route that performs unnecessary database operations can have acceptable average latency under light traffic and terrible p95 or p99 latency under load.&lt;/p&gt;

&lt;p&gt;This is why N+1 problems often become obvious only after an application starts receiving meaningful traffic.&lt;br&gt;
The application didn’t suddenly become badly written. The workload exposed an inefficiency that was already present.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ORM Didn’t Lie. It Hid the Cost.
&lt;/h2&gt;

&lt;p&gt;Let’s be precise about the title.&lt;/p&gt;

&lt;p&gt;Your ORM isn’t necessarily generating bad SQL. In the example above, each individual SQL statement is reasonable.&lt;/p&gt;

&lt;p&gt;The problem is the way the application uses the ORM.&lt;/p&gt;

&lt;p&gt;Object-relational mappers make relational data easier to work with by representing tables and relationships as application-level objects. That abstraction is useful, but it can make the cost of accessing related records less obvious.&lt;/p&gt;

&lt;p&gt;Consider this TypeORM example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8sgv1gr3utb65myhasw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8sgv1gr3utb65myhasw.png" alt=" " width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repository methods are straightforward. The relationship is expressed through objects rather than handwritten SQL.&lt;/p&gt;

&lt;p&gt;But the database still sees a query for the users followed by a query for each user’s projects.&lt;/p&gt;

&lt;p&gt;The same pattern can appear in Prisma, TypeORM, Sequelize, Django ORM, Hibernate, and other data-access tools.&lt;/p&gt;

&lt;p&gt;The syntax changes. The database workload does not.&lt;/p&gt;

&lt;p&gt;This is an important distinction: &lt;strong&gt;an ORM can simplify how you express a query without guaranteeing that the overall query strategy is efficient.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A method call that looks like a single operation at the application level may generate multiple SQL statements. And a loop containing a database call can multiply those statements in ways that are easy to overlook during code review.&lt;/p&gt;

&lt;p&gt;The right question isn’t simply, “Does this code use an ORM correctly?”&lt;/p&gt;

&lt;p&gt;It’s also, “How many database operations does this code generate for a typical request?”&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Fix: Fetch Related Data Intentionally
&lt;/h2&gt;

&lt;p&gt;Let’s return to the original requirement.&lt;/p&gt;

&lt;p&gt;We want to retrieve users and their projects.&lt;/p&gt;

&lt;p&gt;We don’t actually need to retrieve each user’s projects independently. We already know the complete set of users we’re interested in.&lt;br&gt;
That gives us a few better options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option A: Use a nested relation query
&lt;/h2&gt;

&lt;p&gt;With Prisma, you can request the related projects as part of the same logical query:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fihymimakqqbjgimhq6it.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fihymimakqqbjgimhq6it.png" alt=" " width="800" height="853"&gt;&lt;/a&gt;&lt;br&gt;
Instead of manually fetching projects inside a loop, you describe the response shape you need.&lt;/p&gt;

&lt;p&gt;Depending on the ORM version and relation-loading strategy, Prisma may load the related records using a database join or multiple batched SQL queries. Either way, this avoids the original pattern of issuing one independent project query for every user.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;A nested ORM query doesn’t universally mean one SQL statement. Some ORMs fetch relations using a join; others issue separate queries and combine the results in application code. Some support both strategies.&lt;/p&gt;

&lt;p&gt;What matters first is eliminating the repeated per-record lookup. After that, you can evaluate which relation-loading strategy best fits the workload.&lt;/p&gt;

&lt;p&gt;This Prisma’s &lt;a href="https://www.prisma.io/docs/orm/v7/prisma-client/queries/relation-queries" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; explains the available relation-query strategies and their trade-offs&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Fetch the parent records, then batch the children
&lt;/h3&gt;

&lt;p&gt;Sometimes you need more control over how the data is assembled.&lt;/p&gt;

&lt;p&gt;You can retrieve the users first, collect their IDs, and fetch all matching projects in a single additional query.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6boxmhhg4c2d2pw1zeqa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6boxmhhg4c2d2pw1zeqa.png" alt=" " width="800" height="1480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Assume userId is a numeric field in this example. If your schema uses UUIDs or strings, use the corresponding TypeScript type.&lt;/p&gt;

&lt;p&gt;The important part is this filter:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;where: {&lt;br&gt;
  userId: {&lt;br&gt;
    in: userIds,&lt;br&gt;
  },&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Instead of executing one query per user, the application asks PostgreSQL to retrieve projects belonging to the entire set of user IDs.&lt;/p&gt;

&lt;p&gt;For a non-empty user list, this approach uses two logical database reads: one for users and one for projects.&lt;/p&gt;

&lt;p&gt;The application then groups the projects by userId and assembles the response.&lt;/p&gt;

&lt;p&gt;This is a useful pattern when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need custom response shaping.&lt;/li&gt;
&lt;li&gt;You want separate control over parent and child queries.&lt;/li&gt;
&lt;li&gt;The relation-loading behaviour of your ORM isn’t suitable for the endpoint.&lt;/li&gt;
&lt;li&gt;You need to reuse the fetched records for additional application-level processing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are trade-offs. You now own the grouping logic, and a large list of IDs may require batching or pagination. You also need to ensure that the response doesn’t load an unbounded number of child records.&lt;/p&gt;

&lt;p&gt;But the query count no longer grows by one for every user.&lt;/p&gt;

&lt;p&gt;That’s the key improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  A JOIN Is Not Automatically the Best Answer
&lt;/h2&gt;

&lt;p&gt;At this point, it can be tempting to conclude that every N+1 problem should be fixed with a SQL JOIN.&lt;br&gt;
Often, a join is a good solution. But treating it as a universal rule replaces one simplistic assumption with another.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5275uw8narwue3n2iv6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5275uw8narwue3n2iv6.png" alt=" " width="704" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This retrieves users and their projects through a single SQL statement.&lt;/p&gt;

&lt;p&gt;For a straightforward one-to-many relationship, it can be an effective solution.&lt;/p&gt;

&lt;p&gt;But joins change the shape of the result set.&lt;/p&gt;

&lt;p&gt;If one user has ten projects, that user’s columns can appear across ten result rows. If you join multiple one-to-many relationships, the number of intermediate rows can grow dramatically.&lt;/p&gt;

&lt;p&gt;For example, a user has 10 projects and 20 notifications. Joining both collections without carefully controlling the query shape can produce up to 200 combinations for that user.&lt;/p&gt;

&lt;p&gt;This is sometimes called row multiplication or join explosion.&lt;/p&gt;

&lt;p&gt;The database may still execute the query efficiently for a particular workload, but the application can end up processing far more data than it actually needs.&lt;/p&gt;

&lt;p&gt;The practical lesson is not to avoid joins. It is to understand what the join produces.&lt;/p&gt;

&lt;p&gt;Use a join when the relationship and result shape make sense. Consider separate batched queries when they avoid unnecessary row multiplication or make pagination easier to reason about. Measure both approaches when the workload justifies it.&lt;/p&gt;

&lt;p&gt;The objective is not to minimize the SQL statement count at any cost. It is to minimize unnecessary work while preserving the data and consistency your endpoint requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Adding an Index Doesn’t Fix the Root Problem
&lt;/h2&gt;

&lt;p&gt;Suppose the original query is slow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhqccdlm971o8jt69eku6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhqccdlm971o8jt69eku6.png" alt=" " width="799" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That may be the right index for the query, depending on the table size, data distribution, and execution plan.&lt;/p&gt;

&lt;p&gt;But the N+1 pattern remains.&lt;/p&gt;

&lt;p&gt;If the application retrieves 100 users and issues one project query for each, the database still receives 100 project queries. The index may make those lookups faster, but it doesn’t remove their round-trips or execution overhead.&lt;/p&gt;

&lt;p&gt;There is another detail worth remembering: indexes aren’t free. They consume storage and add maintenance work to inserts, updates, and deletes.&lt;/p&gt;

&lt;p&gt;An index should support a &lt;a href="https://dev.to/nsikanadaowo/why-your-database-design-should-start-with-access-patterns-not-tables-6cc"&gt;meaningful access pattern&lt;/a&gt;, not compensate blindly for a query strategy that should be redesigned.&lt;/p&gt;

&lt;p&gt;A useful debugging sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Count how many times the query executes per request.&lt;/li&gt;
&lt;li&gt;Identify whether the repetitions are necessary.&lt;/li&gt;
&lt;li&gt;Change the query shape to batch or load the related data appropriately.&lt;/li&gt;
&lt;li&gt;Inspect the execution plan of the remaining important queries.&lt;/li&gt;
&lt;li&gt;Add or adjust indexes when the evidence supports doing so.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use PostgreSQL’s EXPLAIN (ANALYZE, BUFFERS) to investigate query execution, remembering that ANALYZE executes the statement. For statements that modify data, take appropriate precautions and understand the side effects before running it.&lt;/p&gt;

&lt;p&gt;Query-plan analysis and query-count analysis answer different questions. You need both when both are relevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GraphQL Trap: One Resolver, One Query, Repeated Many Times
&lt;/h2&gt;

&lt;p&gt;N+1 problems are particularly easy to introduce in GraphQL.&lt;/p&gt;

&lt;p&gt;Imagine a GraphQL API that returns users and their projects:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;query {&lt;br&gt;
  users {&lt;br&gt;
    id&lt;br&gt;
    name&lt;br&gt;
    projects {&lt;br&gt;
      id&lt;br&gt;
      name&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The users resolver fetches the users.&lt;/p&gt;

&lt;p&gt;Then the projects field resolver runs for each returned user.&lt;/p&gt;

&lt;p&gt;If that resolver performs an independent database query, the request can produce the familiar N+1 pattern.&lt;/p&gt;

&lt;p&gt;The confusing part is that each resolver may be perfectly reasonable in isolation.&lt;/p&gt;

&lt;p&gt;The users resolver knows how to fetch users. The projects resolver knows how to fetch projects for a user. Both do their jobs correctly.&lt;/p&gt;

&lt;p&gt;The inefficiency appears when the GraphQL execution model invokes the projects resolver repeatedly without coordinating those database reads.&lt;/p&gt;

&lt;p&gt;One common solution is a DataLoader-style batching mechanism.&lt;/p&gt;

&lt;p&gt;Conceptually, the resolver asks for projects for a particular user:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const projects = await projectLoader.load(user.id);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Instead of immediately executing a separate query for every call, the loader collects multiple user IDs and fetches the corresponding projects in a batch.&lt;/p&gt;

&lt;p&gt;The batch function might look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fogtw26j9n5vu8rn5cssy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fogtw26j9n5vu8rn5cssy.png" alt=" " width="800" height="1079"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The returned array must correspond to the order of the incoming IDs, including empty results. That ordering contract is important for DataLoader implementations.&lt;/p&gt;

&lt;p&gt;In production, you would normally use a DataLoader implementation or equivalent batching utility rather than building all of its caching and scheduling behavior yourself.&lt;/p&gt;

&lt;p&gt;Also, create loaders with the appropriate request scope when cached results depend on the current user or authorization context. A global loader cache can accidentally reuse data across requests if it’s designed incorrectly.&lt;/p&gt;

&lt;p&gt;DataLoader isn’t just a performance trick. It addresses a structural problem: field-level resolvers often need a way to coordinate data access across many related objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Promise.all() Is Not a Real Fix
&lt;/h2&gt;

&lt;p&gt;One of the most common attempts to fix a slow loop is to make the queries concurrent.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxyhcjo3hhvuydjiuw67o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxyhcjo3hhvuydjiuw67o.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A developer writes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc1mrlqtkmx9oa57o3334.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc1mrlqtkmx9oa57o3334.png" alt=" " width="800" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second implementation removes the sequential wait between queries.&lt;/p&gt;

&lt;p&gt;That can reduce wall-clock latency for small, bounded workloads. But it still issues one query per user.&lt;/p&gt;

&lt;p&gt;With 100 users, you’re still generating roughly 100 project queries.&lt;/p&gt;

&lt;p&gt;Worse, you’re now attempting to issue them concurrently. Depending on the database pool and workload, this can increase contention, create a queue of waiting queries, and consume resources more aggressively.&lt;/p&gt;

&lt;p&gt;Concurrency changes when the work runs. It does not eliminate the work.&lt;/p&gt;

&lt;p&gt;There are legitimate reasons to execute independent queries concurrently. The mistake is using concurrency as a substitute for reducing unnecessary database operations.&lt;/p&gt;

&lt;p&gt;Before asking, “How can I make these queries run faster?”, ask, “Why am I running so many queries at all?”&lt;/p&gt;

&lt;h2&gt;
  
  
  Not Every Repeated Query Is an N+1 Problem
&lt;/h2&gt;

&lt;p&gt;A good engineer also knows when not to apply a pattern mechanically.&lt;/p&gt;

&lt;p&gt;Imagine you’re processing a small batch of three records. Fetching related data separately might be perfectly acceptable if it makes the code simpler and the performance requirements are met.&lt;/p&gt;

&lt;p&gt;Or suppose each item requires a different query with distinct filters, authorization rules, or business logic. Those queries may not be safely reducible to one batch.&lt;/p&gt;

&lt;p&gt;The defining concern isn’t simply that a query appears multiple times. It’s that a query is being repeated per result when the underlying data could be retrieved more efficiently as a group.&lt;/p&gt;

&lt;p&gt;You should also distinguish N+1 from other database performance problems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing indexes:&lt;/strong&gt; The database must examine too many rows to answer a query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-fetching:&lt;/strong&gt; The application retrieves more columns or records than it needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inefficient joins:&lt;/strong&gt; A query creates excessive intermediate rows or performs expensive operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection pool exhaustion:&lt;/strong&gt; Requests wait for database connections to become available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;N+1 queries:&lt;/strong&gt; The application issues unnecessary additional queries as the number of parent records grows.
These problems can occur together, but they require different diagnoses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A slow endpoint is a symptom, not a diagnosis. Don’t start adding indexes, introducing Redis, or rewriting your ORM queries until you understand which cost is actually dominating the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Detect N+1 Queries Before Production
&lt;/h2&gt;

&lt;p&gt;The most reliable approach is to observe what the application sends to the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Log Prisma queries during development
&lt;/h3&gt;

&lt;p&gt;Prisma supports query-event logging that can help you inspect generated SQL and execution durations.&lt;br&gt;
For example, depending on your Prisma version and client setup:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foj9bsj9t2s531ooitqbg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foj9bsj9t2s531ooitqbg.png" alt=" " width="800" height="869"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a simplified example of setting up a query listener. In a NestJS application, integrate the listener into your Prisma service or database instrumentation setup rather than creating a separate client for every service.&lt;/p&gt;

&lt;p&gt;Avoid indiscriminately logging sensitive parameter values in production. Query logs &lt;a href="https://www.prisma.io/docs/orm/v7/prisma-client/queries/advanced/query-optimization-performance" rel="noopener noreferrer"&gt;can expose personal information or other confidential data&lt;/a&gt;, and excessive logging can introduce its own performance overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Look for repeated query shapes
&lt;/h3&gt;

&lt;p&gt;Suppose your logs contain:&lt;/p&gt;

&lt;p&gt;`SELECT ... FROM "User";&lt;/p&gt;

&lt;p&gt;SELECT ... FROM "Project"&lt;br&gt;
WHERE "userId" = $1;&lt;/p&gt;

&lt;p&gt;SELECT ... FROM "Project"&lt;br&gt;
WHERE "userId" = $1;&lt;/p&gt;

&lt;p&gt;SELECT ... FROM "Project"&lt;br&gt;
WHERE "userId" = $1;`&lt;/p&gt;

&lt;p&gt;The repeated query shape is a clue.&lt;/p&gt;

&lt;p&gt;The parameter changes, but the SQL structure stays the same. If the same route triggers this pattern repeatedly, investigate whether the query is being executed once for every parent record.&lt;/p&gt;

&lt;p&gt;Don’t assume that repeated SQL automatically means N+1. A query may legitimately execute multiple times. Correlate the events with the request, route, and data-access code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measure query count per request
&lt;/h3&gt;

&lt;p&gt;Execution duration alone doesn’t tell the full story.&lt;/p&gt;

&lt;p&gt;A route that executes 40 queries taking 2 milliseconds each may look healthy when you inspect individual query durations. Yet the route could be doing far more database work than its response requires.&lt;/p&gt;

&lt;p&gt;Useful measurements include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of database queries per request.&lt;/li&gt;
&lt;li&gt;Repeated query shapes and execution counts.&lt;/li&gt;
&lt;li&gt;Total database time per request.&lt;/li&gt;
&lt;li&gt;Endpoint latency, including p50, p95, and p99.&lt;/li&gt;
&lt;li&gt;Connection pool wait time and utilization.&lt;/li&gt;
&lt;li&gt;Rows returned and, where available, rows examined or read.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Test with realistic data volumes
&lt;/h3&gt;

&lt;p&gt;An N+1 problem may be invisible when your test database contains only two users.&lt;/p&gt;

&lt;p&gt;With two users, the endpoint performs three queries. That may seem harmless.&lt;/p&gt;

&lt;p&gt;With 200 users, the same code performs 201 queries.&lt;/p&gt;

&lt;p&gt;Performance tests should use representative record counts and relationships. Where practical, test query counts as well as response time. A test that verifies only the returned JSON can pass even when the implementation generates hundreds of unnecessary queries. &lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Checklist for Reviewing Database Access
&lt;/h2&gt;

&lt;p&gt;When reviewing a NestJS service, GraphQL resolver, or repository method, ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Am I querying inside a loop?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Look for database calls inside for, for...of, map, forEach, or other repeated execution paths. Not every occurrence is wrong, but each deserves scrutiny.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Can I fetch the related records in one batch?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you already have a collection of IDs, consider a query using IN, an ORM’s nested relation loading, or a DataLoader-style batching mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Am I returning only the data the endpoint needs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use select or the equivalent to avoid loading large text fields, unnecessary relations, or entire entities when the response requires only a few fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Is the response bounded?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pagination matters. Fetching all users and all their projects in one giant query can replace an N+1 problem with an unbounded data-loading problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Does the chosen loading strategy fit the relationship?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A join may be effective for one relation but expensive when several large collections are loaded together. Compare the query shapes and actual execution costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Have I measured the result?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Count the queries before and after the change. Compare endpoint latency and database resource usage under representative load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Is the data access still correct?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Optimization must preserve authorization, filtering, ordering, pagination, and any required consistency guarantees. A faster query that returns the wrong records is not an optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts: Your ORM Is Only as Smart as the Queries You Ask It to Make
&lt;/h2&gt;

&lt;p&gt;N+1 queries are frustrating because they rarely look like a serious engineering mistake at first.&lt;/p&gt;

&lt;p&gt;The code is readable. The relationships are modeled correctly. The endpoint returns the expected response.&lt;/p&gt;

&lt;p&gt;The problem is hidden in the execution pattern.&lt;/p&gt;

&lt;p&gt;An ORM can make database interactions easier to express, but it cannot remove the need to understand how those interactions translate into actual database work.&lt;/p&gt;

&lt;p&gt;And the fix isn’t always to write raw SQL, add an index, introduce a cache, or force every relation into a join. Sometimes, the most effective improvement is simply to stop asking the database the same question one record at a time.&lt;/p&gt;

&lt;p&gt;Think about the workload your code creates, not just the code you write.&lt;/p&gt;

&lt;p&gt;When an endpoint becomes slow, don’t stop at asking whether each query is efficient. Ask whether all those queries need to exist in the first place.&lt;/p&gt;

&lt;p&gt;Because the fastest unnecessary query is the one your application never sends.&lt;/p&gt;

</description>
      <category>backenddevelopment</category>
      <category>databasedesign</category>
      <category>architecture</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How to Choose a Database for Your Next SaaS: A Practical Framework</title>
      <dc:creator>Nsikan Patrick Adaowo</dc:creator>
      <pubDate>Thu, 22 Oct 2026 19:45:39 +0000</pubDate>
      <link>https://dev.to/nsikanadaowo/how-to-choose-a-database-for-your-next-saas-a-practical-framework-ko1</link>
      <guid>https://dev.to/nsikanadaowo/how-to-choose-a-database-for-your-next-saas-a-practical-framework-ko1</guid>
      <description>&lt;p&gt;“Which database is best for my SaaS?”&lt;/p&gt;

&lt;p&gt;This is a reasonable question, but it’s also the wrong place to start.&lt;/p&gt;

&lt;p&gt;PostgreSQL, MongoDB, Redis, and other databases solve different problems, operate with different trade-offs, and excel under different workloads. Comparing their features without understanding your application is like choosing an architecture before defining what the system needs to do.&lt;/p&gt;

&lt;p&gt;Will your application perform complex joins? Process thousands of writes per second? Maintain strict consistency across financial records? Or retrieve flexible documents with minimal transformation?&lt;/p&gt;

&lt;p&gt;Each answer changes the decision.&lt;/p&gt;

&lt;p&gt;There is no universally best database. There is only the database whose trade-offs make the most sense for the system you’re building.&lt;/p&gt;

&lt;p&gt;The challenge is knowing how to evaluate those trade-offs before committing to a technology. This article discusses what you need to know before picking a database for your SaaS product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With the Product, Not the Database
&lt;/h2&gt;

&lt;p&gt;Imagine you’re building a subscription platform for small online stores. Each store can manage products, orders, customers, and staff. Later, you may add reporting, search, and integrations.&lt;/p&gt;

&lt;p&gt;It’s tempting to begin with a technology list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL for data.&lt;/li&gt;
&lt;li&gt;Redis for caching.&lt;/li&gt;
&lt;li&gt;Elasticsearch for search.&lt;/li&gt;
&lt;li&gt;Kafka for events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But that is not architecture yet. It’s a collection of tools—and every extra system brings its own deployment, monitoring, backup, and failure story.&lt;/p&gt;

&lt;p&gt;First, describe the product’s important data and behaviours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A customer places an order.&lt;/li&gt;
&lt;li&gt;The system reserves or reduces inventory.&lt;/li&gt;
&lt;li&gt;A payment provider confirms or rejects payment.&lt;/li&gt;
&lt;li&gt;Store staff update order status.&lt;/li&gt;
&lt;li&gt;The owner views sales reports.&lt;/li&gt;
&lt;li&gt;A customer searches the store’s products.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now ask which of these behaviours must be correct even when requests overlap, a service restarts, or an external provider is slow.&lt;/p&gt;

&lt;p&gt;That question brings you closer to the database decision than “What’s popular right now?”&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Write Down Your Invariants
&lt;/h2&gt;

&lt;p&gt;An invariant is a rule that must remain true for the system to be correct.&lt;br&gt;
For the store platform, examples might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An order must belong to the correct tenant.&lt;/li&gt;
&lt;li&gt;A completed payment must not be recorded twice.&lt;/li&gt;
&lt;li&gt;An order must not reserve more stock than is available, unless backorders are an explicit feature.&lt;/li&gt;
&lt;li&gt;A staff member must not access another store’s data.&lt;/li&gt;
&lt;li&gt;A refunded payment must remain connected to its original transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These rules are more important than your expected traffic. Traffic can grow; a broken financial invariant can damage trust from day one.&lt;/p&gt;

&lt;p&gt;For each invariant, ask where it should be enforced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a database constraint or transaction?&lt;/li&gt;
&lt;li&gt;In application code?&lt;/li&gt;
&lt;li&gt;At a service boundary?&lt;/li&gt;
&lt;li&gt;In a reconciliation process that detects and repairs discrepancies?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Usually, the answer is not “only one place.” Critical rules often need layered protection. But if the database can enforce an invariant directly, you avoid relying on every code path to remember it.&lt;/p&gt;

&lt;p&gt;For example, a unique constraint on a payment provider’s transaction ID can help prevent duplicate payment records. A transaction can ensure that creating an order and reserving its inventory happen together within the database’s supported boundary.&lt;/p&gt;

&lt;p&gt;The lesson is not that one database makes correctness automatic. It’s that your choice should make your important rules practical to enforce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Choose for Your Access Patterns
&lt;/h2&gt;

&lt;p&gt;Understanding your invariants is only half the equation. You also need to understand how your application will retrieve and modify data.&lt;br&gt;
Two applications can have nearly identical entities but require different database designs because &lt;a href="https://dev.to/nsikanadaowo/why-your-database-design-should-start-with-access-patterns-not-tables-6cc"&gt;their access patterns differ&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Consider a task management application. Its most frequent query might retrieve a user’s open tasks, ordered by due date.&lt;/p&gt;

&lt;p&gt;Another application might mostly retrieve a complete task document by ID.&lt;/p&gt;

&lt;p&gt;The first workload involves filtering and sorting across a set of records. The second may be dominated by direct lookups.&lt;/p&gt;

&lt;p&gt;Those differences affect indexing, query design, and sometimes the appropriate data model.&lt;/p&gt;

&lt;h3&gt;
  
  
  List the Queries That Matter
&lt;/h3&gt;

&lt;p&gt;A data model should serve the ways the product reads and writes data. Write down the important queries before choosing a storage model.&lt;/p&gt;

&lt;p&gt;For our SaaS, that list might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load a store’s products by category.&lt;/li&gt;
&lt;li&gt;Fetch an order and its line items.&lt;/li&gt;
&lt;li&gt;List recent orders for one store, sorted by creation time.&lt;/li&gt;
&lt;li&gt;Find a payment by its provider transaction ID.&lt;/li&gt;
&lt;li&gt;Show a customer’s order history.&lt;/li&gt;
&lt;li&gt;Search products by name.&lt;/li&gt;
&lt;li&gt;Summarize daily sales for a date range.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each query, note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How often it runs.&lt;/li&gt;
&lt;li&gt;How fresh its result must be.&lt;/li&gt;
&lt;li&gt;Whether it needs joins or aggregation.&lt;/li&gt;
&lt;li&gt;How much data it may scan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether it must work across tenants or stay inside one tenant.&lt;/p&gt;

&lt;p&gt;This is where database models start to look different.&lt;/p&gt;

&lt;p&gt;A relational database can make connected records and transactional updates natural. A key-value or document database may be a strong fit when the access patterns are known and can be served efficiently by specific keys and indexes. A search engine can support rich text search, but it does not need to become the authoritative home for orders and payments.&lt;br&gt;
Don’t ask only, “Can this database store the data?” Almost all modern databases can store your data. Ask, “Can it serve the important queries without forcing us to build a second database in application code?”&lt;/p&gt;

&lt;h3&gt;
  
  
  What if the access patterns change?
&lt;/h3&gt;

&lt;p&gt;Your SaaS will evolve. A feature that originally required retrieving a single record may later need filtering, sorting, reporting, or cross-entity searches.&lt;/p&gt;

&lt;p&gt;You cannot predict every future query. But you can identify the application’s core access patterns and avoid choosing a database that makes them unnecessarily difficult.&lt;/p&gt;

&lt;p&gt;This is why workload analysis should happen before technology selection, not after performance problems appear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Understand the Difference Between SQL and NoSQL
&lt;/h2&gt;

&lt;p&gt;SQL versus NoSQL is one of the most common database debates. It is also one of the least useful starting points for choosing a database.&lt;/p&gt;

&lt;p&gt;SQL databases typically offer a relational model, declarative querying, and strong integrity features. NoSQL is a broad category that includes document databases, key-value stores, wide-column databases, and other systems with different data and access models.&lt;/p&gt;

&lt;p&gt;These are not two interchangeable technologies with a single universal winner.&lt;/p&gt;

&lt;p&gt;They represent different design choices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Relational databases
&lt;/h3&gt;

&lt;p&gt;PostgreSQL and MySQL are common choices for SaaS applications involving related entities, transactional workflows, flexible queries, and constraints.&lt;/p&gt;

&lt;p&gt;They are particularly attractive when the application needs to join related data, enforce uniqueness, maintain referential integrity, and evolve its query requirements over time.&lt;/p&gt;

&lt;p&gt;For many conventional SaaS products, PostgreSQL is a strong starting point because it supports a broad range of application workloads without requiring the team to operate several different storage systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Document databases
&lt;/h3&gt;

&lt;p&gt;MongoDB can be a good fit when the document is a natural unit of storage and retrieval, document structures vary meaningfully, and the application’s access patterns align with its document model.&lt;/p&gt;

&lt;p&gt;But flexibility doesn’t mean that relationships, consistency, and query complexity disappear. Those concerns still exist; they simply need to be addressed according to the database’s model and capabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key-value stores
&lt;/h3&gt;

&lt;p&gt;Redis is designed for low-latency access to values by key, with additional data structures and capabilities. It is commonly useful for caching, ephemeral state, rate limiting, and certain queue or coordination patterns.&lt;/p&gt;

&lt;p&gt;It may also serve as a primary datastore for specific workloads, but that decision requires evaluating persistence, recovery, durability, and data-model requirements carefully.&lt;/p&gt;

&lt;p&gt;Choosing Redis because it is fast doesn’t automatically make it the right system of record for customer accounts, invoices, and other business-critical data.&lt;/p&gt;

&lt;h3&gt;
  
  
  The decision isn’t about which category wins
&lt;/h3&gt;

&lt;p&gt;A relational database can handle flexible data. A document database can support relationships and transactions within its supported model. A key-value store can be appropriate for more than simple caching.&lt;/p&gt;

&lt;p&gt;The real question is how well each candidate supports your requirements, including the operational costs and constraints it introduces.&lt;/p&gt;

&lt;p&gt;Avoid making the decision based on labels alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Choose Consistency Deliberately
&lt;/h2&gt;

&lt;p&gt;Not every piece of data needs the same consistency guarantees. But some operations cannot tolerate ambiguous or conflicting outcomes.&lt;/p&gt;

&lt;p&gt;If a store owner changes a product description, it may be acceptable for search results to update a little later. If a customer pays for an order, the system needs a clear and reliable way to record that outcome.&lt;/p&gt;

&lt;p&gt;That suggests a useful distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authoritative data&lt;/strong&gt; is where the system records the truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derived data&lt;/strong&gt; is built from that truth to make particular reads faster or more convenient.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The order and payment records may live in the primary transactional database.&lt;/li&gt;
&lt;li&gt;Product search documents may be copied into a search index.&lt;/li&gt;
&lt;li&gt;Sales summaries may be computed into reporting tables or an analytics store.&lt;/li&gt;
&lt;li&gt;Frequently requested product details may be cached.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The search index can lag behind the product database. That may be acceptable if you’ve chosen that behaviour deliberately. The payment record should not be treated as “eventually correct” without a clear reconciliation and customer-support plan.&lt;/p&gt;

&lt;p&gt;This connects directly to the earlier point: consistency is not merely a technical setting. It becomes a product promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Evaluate Scale Without Prematurely Optimizing for It
&lt;/h2&gt;

&lt;p&gt;Developers sometimes choose a database for the scale they hope to reach rather than the workload they actually have.&lt;/p&gt;

&lt;p&gt;A new SaaS might adopt a distributed database because it expects millions of users. Another might introduce multiple databases, a cache, a search engine, and a message broker before the first customer arrives.&lt;/p&gt;

&lt;p&gt;These decisions can create operational overhead long before they deliver meaningful value.&lt;/p&gt;

&lt;p&gt;A better approach is to distinguish between the scale you need today, the growth you reasonably expect, and the requirements that would force a different architecture later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Think beyond the number of users
&lt;/h3&gt;

&lt;p&gt;User count alone is a poor measure of database workload.&lt;/p&gt;

&lt;p&gt;Ten thousand users who occasionally update records may create less database pressure than a few hundred users generating thousands of events per second.&lt;/p&gt;

&lt;p&gt;Evaluate factors such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read and write throughput.&lt;/li&gt;
&lt;li&gt;Peak traffic and burstiness.&lt;/li&gt;
&lt;li&gt;Dataset size and growth rate.&lt;/li&gt;
&lt;li&gt;Query complexity and data distribution.&lt;/li&gt;
&lt;li&gt;Latency requirements.&lt;/li&gt;
&lt;li&gt;Replication and availability requirements.&lt;/li&gt;
&lt;li&gt;Backup and recovery objectives.&lt;/li&gt;
&lt;li&gt;Retention and archival needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For many early-stage SaaS products, a well-designed PostgreSQL database on a managed service is sufficient to support substantial growth.&lt;/p&gt;

&lt;p&gt;That doesn’t mean PostgreSQL is always the right answer or that it scales indefinitely without architectural changes. It means you should measure your requirements before introducing complexity.&lt;/p&gt;

&lt;p&gt;You can often extend a sound starting architecture through better indexing, query optimization, connection pooling, caching, read replicas, partitioning, or workload separation.&lt;/p&gt;

&lt;p&gt;Each option has trade-offs, and none is a substitute for understanding the bottleneck.&lt;/p&gt;

&lt;p&gt;Don’t pay the complexity cost of a distributed architecture until your requirements justify it—or you have a clear, concrete reason to adopt it early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Account for Operational Complexity
&lt;/h2&gt;

&lt;p&gt;A database doesn’t exist only in your application code. Someone has to run it, monitor it, secure it, back it up, restore it, and respond when something goes wrong.&lt;/p&gt;

&lt;p&gt;For an early-stage SaaS team, those responsibilities can matter as much as query performance.&lt;/p&gt;

&lt;p&gt;Consider two database options.&lt;/p&gt;

&lt;p&gt;One offers excellent performance for a specialized workload but requires your team to manage clustering, replication, upgrades, and recovery.&lt;/p&gt;

&lt;p&gt;The other is a managed service that provides automated backups, monitoring, routine maintenance, and a straightforward recovery process.&lt;/p&gt;

&lt;p&gt;The specialized option might be justified if its capabilities are essential. Otherwise, the managed service could be the better engineering decision—even if it isn’t the theoretical winner on every benchmark.&lt;/p&gt;

&lt;p&gt;When evaluating operational fit, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can we back up and restore the database reliably?&lt;/li&gt;
&lt;li&gt;How difficult is it to monitor query performance and resource usage?&lt;/li&gt;
&lt;li&gt;What happens when the database becomes unavailable?&lt;/li&gt;
&lt;li&gt;How do we handle schema migrations safely?&lt;/li&gt;
&lt;li&gt;What are the limits of our managed service?&lt;/li&gt;
&lt;li&gt;Can the team diagnose problems with its current skills?&lt;/li&gt;
&lt;li&gt;What happens if we need to migrate away from this provider?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last question deserves particular attention.&lt;/p&gt;

&lt;p&gt;Portability is not just about whether a database can export data. It also involves query languages, database-specific features, extensions, migration tooling, and assumptions embedded in application code.&lt;/p&gt;

&lt;p&gt;Using a familiar database doesn’t eliminate operational risk, but it can reduce the number of unfamiliar problems your team must solve simultaneously.&lt;/p&gt;

&lt;p&gt;For a small team, that reduction in complexity is often valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Calculate the Total Cost, Not Just the Hosting Bill
&lt;/h2&gt;

&lt;p&gt;Database costs extend beyond the monthly invoice from your cloud provider.&lt;/p&gt;

&lt;p&gt;A database may have inexpensive storage but require more compute for your query workload. Another may offer attractive performance but incur costs for replication, backup retention, data transfer, or specialized infrastructure.&lt;/p&gt;

&lt;p&gt;There is also the cost of engineering time.&lt;/p&gt;

&lt;p&gt;A database that requires substantial custom code to support ordinary application operations can be more expensive in practice than one with a higher hosting bill.&lt;/p&gt;

&lt;p&gt;Your evaluation should include:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure costs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compute, storage, and I/O.&lt;/li&gt;
&lt;li&gt;Replication and high availability.&lt;/li&gt;
&lt;li&gt;Backups and retention.&lt;/li&gt;
&lt;li&gt;Data transfer and regional deployment.&lt;/li&gt;
&lt;li&gt;Managed-service pricing and usage limits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Engineering costs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Development and migration effort.&lt;/li&gt;
&lt;li&gt;Query optimization and maintenance.&lt;/li&gt;
&lt;li&gt;Monitoring and incident response.&lt;/li&gt;
&lt;li&gt;Specialized operational knowledge.&lt;/li&gt;
&lt;li&gt;Future changes to the data model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an early-stage product, operational simplicity and predictable costs may be more important than squeezing out the last bit of performance.&lt;/p&gt;

&lt;p&gt;As the workload grows, that balance may change.&lt;/p&gt;

&lt;p&gt;The point isn’t to predict the exact lifetime cost before launch. It’s to identify the major cost drivers and avoid a choice that makes your likely workload unnecessarily expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Use Specialized Databases Only When They Solve a Real Problem
&lt;/h2&gt;

&lt;p&gt;A growing SaaS may eventually benefit from more than one database technology.&lt;/p&gt;

&lt;p&gt;For example, you might use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL as the primary system of record.&lt;/li&gt;
&lt;li&gt;Redis for caching or short-lived state.&lt;/li&gt;
&lt;li&gt;A dedicated search engine for sophisticated full-text search.&lt;/li&gt;
&lt;li&gt;An analytical store for large-scale reporting.&lt;/li&gt;
&lt;li&gt;A vector-search capability for semantic retrieval in an AI-powered feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These systems serve different purposes. But adding them also means more infrastructure, more failure modes, more monitoring, and more data synchronization to manage.&lt;/p&gt;

&lt;p&gt;Imagine your SaaS needs semantic search over product descriptions. That requirement alone doesn’t mean you must immediately adopt a separate vector database.&lt;/p&gt;

&lt;p&gt;Depending on the scale and retrieval requirements, PostgreSQL with the pgvector extension may be sufficient. A dedicated vector database may become attractive when its performance, indexing, filtering, scaling, or operational characteristics better fit the workload.&lt;/p&gt;

&lt;p&gt;The same reasoning applies to caching and search infrastructure.&lt;/p&gt;

&lt;p&gt;Don’t add a technology because it appears in the architecture of a popular company. Add it because you can identify the problem it solves, explain why your existing system is insufficient, and justify the additional complexity.&lt;/p&gt;

&lt;p&gt;This is particularly important for AI-powered SaaS products. Embeddings, retrieval, conversation history, and background inference jobs introduce additional workloads, but they don’t automatically require replacing your primary relational database.&lt;/p&gt;

&lt;p&gt;Start with the simplest architecture that meets the actual requirements. Expand when evidence or a concrete requirement justifies it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Framework for Making the Decision
&lt;/h2&gt;

&lt;p&gt;You can use this framework when starting a new SaaS project or reconsidering an existing database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define the application’s core operations
&lt;/h3&gt;

&lt;p&gt;List the reads, writes, filters, aggregations, and background jobs that matter most.&lt;/p&gt;

&lt;p&gt;Don’t stop at entities such as User, Order, and Product. Describe the operations the application must perform on them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Identify the data guarantees
&lt;/h3&gt;

&lt;p&gt;Determine which records must remain consistent, which relationships need enforcement, and which operations must be atomic.&lt;/p&gt;

&lt;p&gt;Document uniqueness rules, transactional boundaries, concurrency risks, and recovery requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Estimate the workload
&lt;/h3&gt;

&lt;p&gt;Make reasonable estimates of traffic, data volume, write frequency, latency, and growth.&lt;/p&gt;

&lt;p&gt;Separate current requirements from speculative future requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Shortlist suitable database candidates
&lt;/h3&gt;

&lt;p&gt;Choose a small number of technologies that can support your core workload.&lt;/p&gt;

&lt;p&gt;For a conventional transactional SaaS, PostgreSQL is often a sensible first candidate. Consider alternatives when your data model, access patterns, or specialized requirements give them a meaningful advantage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Test the critical operations
&lt;/h3&gt;

&lt;p&gt;Build a small proof of concept for your most important or difficult queries.&lt;/p&gt;

&lt;p&gt;Use realistic data distributions, inspect query plans, and test the consistency behaviour of critical operations. Benchmarks using tiny datasets or unrealistic access patterns can be misleading.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Evaluate operational and financial costs
&lt;/h3&gt;

&lt;p&gt;Compare managed-service options, backup and recovery capabilities, monitoring, migration complexity, and expected infrastructure costs.&lt;/p&gt;

&lt;p&gt;Include the team’s ability to operate the system reliably.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Document the trade-off
&lt;/h3&gt;

&lt;p&gt;Record why you chose the database, which alternatives you considered, what assumptions informed the decision, and which signals would cause you to reconsider it.&lt;/p&gt;

&lt;p&gt;This makes the decision easier to revisit when the product evolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Sensible Starting Point for Your Next SaaS Product
&lt;/h2&gt;

&lt;p&gt;For your next SaaS product, a practical initial architecture might look like this:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. PostgreSQL as the primary database
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is a strong starting candidate because the product contains relational data, transactional workflows, constraints, and a variety of query patterns.&lt;/p&gt;

&lt;p&gt;It can support the core entities, transactional operations, filtering, joins, reporting queries, and flexible attributes where JSONB is useful.&lt;/p&gt;

&lt;p&gt;Prisma can provide a convenient TypeScript data-access layer, while NestJS can organize the application logic and transaction boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Redis when a clear use case appears
&lt;/h3&gt;

&lt;p&gt;You might later introduce Redis for frequently accessed cached data, rate limiting, or short-lived state.&lt;/p&gt;

&lt;p&gt;But the application doesn’t need Redis simply because it has a database. First identify a workload where caching or low-latency key-based access provides a measurable benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Specialized infrastructure when requirements justify it
&lt;/h3&gt;

&lt;p&gt;If reporting workloads begin interfering with transactional queries, consider separating analytical workloads. If full-text or semantic search outgrows the existing solution, evaluate a dedicated search system or vector database.&lt;/p&gt;

&lt;p&gt;Those decisions should follow actual requirements, not precede them.&lt;/p&gt;

&lt;h3&gt;
  
  
  What makes this a good decision?
&lt;/h3&gt;

&lt;p&gt;It isn’t that PostgreSQL is always the best SaaS database.&lt;/p&gt;

&lt;p&gt;It’s that PostgreSQL fits the stated requirements, supports a broad range of the application’s operations, and allows the team to start with a relatively simple architecture.&lt;/p&gt;

&lt;p&gt;This is not a universal prescription. It is a reasonable starting shape for a product with connected business data, transactional workflows, and a small team that wants to keep its operational surface manageable.&lt;/p&gt;

&lt;p&gt;The important part is the boundary: the primary database owns the authoritative business records; specialized systems serve specialized workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Decision Checklist
&lt;/h2&gt;

&lt;p&gt;Before committing to a database, answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What are the system’s most important invariants?&lt;/li&gt;
&lt;li&gt;Which operations need to be atomic?&lt;/li&gt;
&lt;li&gt;What are the top queries the product must serve?&lt;/li&gt;
&lt;li&gt;Which reads can tolerate stale or derived data?&lt;/li&gt;
&lt;li&gt;How will tenant isolation work?&lt;/li&gt;
&lt;li&gt;What backup and restore process can the team actually run?&lt;/li&gt;
&lt;li&gt;What is the likely first scaling bottleneck?&lt;/li&gt;
&lt;li&gt;Which extra components are needed now, rather than merely imaginable?&lt;/li&gt;
&lt;li&gt;What concrete signal would make us revisit this decision?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If these questions have clear answers, you’re not choosing a database based on hype or habit. You’re making a decision that fits the product and the team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal Isn’t a Perfect Choice
&lt;/h2&gt;

&lt;p&gt;There is no database that makes every workload simple, every query fast, and every failure harmless.&lt;/p&gt;

&lt;p&gt;The goal is to choose a system whose trade-offs you understand—and whose constraints match the product you’re building now. Then keep the decision visible as the product changes.&lt;/p&gt;

&lt;p&gt;A database decision is not a lifetime contract. But changing it later is rarely just swapping a connection string. It can mean rewriting queries, moving data, changing consistency guarantees, updating operational practices, and revisiting service boundaries.&lt;/p&gt;

&lt;p&gt;So don’t choose a database because someone said it scales. Don’t reject one because it might not scale to a future you haven’t reached.&lt;/p&gt;

&lt;p&gt;Start with the invariants. Map the access patterns. Be honest about your team’s operational capacity. Then choose the simplest architecture that supports the product’s real requirements.&lt;/p&gt;

&lt;p&gt;That’s not under-engineering. It’s making the decision on purpose.&lt;/p&gt;

</description>
      <category>backenddevelopment</category>
      <category>databasedesign</category>
      <category>softwareachitecture</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Why Your Database Design Should Start with Access Patterns — Not Tables.</title>
      <dc:creator>Nsikan Patrick Adaowo</dc:creator>
      <pubDate>Sun, 18 Oct 2026 17:49:44 +0000</pubDate>
      <link>https://dev.to/nsikanadaowo/why-your-database-design-should-start-with-access-patterns-not-tables-6cc</link>
      <guid>https://dev.to/nsikanadaowo/why-your-database-design-should-start-with-access-patterns-not-tables-6cc</guid>
      <description>&lt;p&gt;There’s a familiar way developers approach database design.&lt;/p&gt;

&lt;p&gt;Identify the entities. Create the tables. Define the relationships. Add a few indexes. Connect everything to an ORM and start building endpoints.&lt;/p&gt;

&lt;p&gt;Users need a table. Projects need a table. Tasks need a table. Each task belongs to a project, so we add a foreign key. Everything looks clean, and the application starts working.&lt;/p&gt;

&lt;p&gt;Then the requirements become more realistic.&lt;/p&gt;

&lt;p&gt;You need to retrieve a user’s most recent tasks across multiple projects. You need to display the latest activity in a workspace. You need to filter tasks by status, sort them by priority, and paginate the results. You need to generate monthly reports without slowing down ordinary requests.&lt;/p&gt;

&lt;p&gt;Suddenly, the schema that looked perfectly reasonable starts producing complicated queries, unnecessary database round trips, and performance problems that are difficult to explain.&lt;/p&gt;

&lt;p&gt;The instinct is often to add another index, introduce caching, or restructure a few tables.&lt;/p&gt;

&lt;p&gt;Sometimes those are the right solutions.&lt;/p&gt;

&lt;p&gt;But most times the problem started much earlier: you designed the database around the entities you could identify, not the operations your application needed to perform.&lt;br&gt;
A database schema isn’t just a representation of your business objects. It also determines how efficiently the application can retrieve, update, and maintain the data those objects represent.&lt;/p&gt;

&lt;p&gt;This is why good database design starts with access patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Entities Tell You What Exists. Access Patterns Tell You What Matters.
&lt;/h2&gt;

&lt;p&gt;Imagine you’re building a project management application.&lt;/p&gt;

&lt;p&gt;At the domain level, you identify a few obvious entities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users&lt;/li&gt;
&lt;li&gt;Workspaces&lt;/li&gt;
&lt;li&gt;Projects&lt;/li&gt;
&lt;li&gt;Tasks&lt;/li&gt;
&lt;li&gt;Comments&lt;/li&gt;
&lt;li&gt;Activity logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reasonable relational model might connect them like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7u0796i8m4eevhd1v54.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7u0796i8m4eevhd1v54.png" alt=" " width="800" height="674"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This gives you a useful starting point. You understand the major entities and how they relate.&lt;/p&gt;

&lt;p&gt;But it doesn’t tell you enough to design the database well.&lt;/p&gt;

&lt;p&gt;Consider what the application actually needs to do.&lt;/p&gt;

&lt;p&gt;A user opens a workspace dashboard and expects to see its projects, outstanding tasks, and recent activity. Another user opens a project and expects to see its tasks, ordered by priority. A manager wants all overdue tasks assigned to a particular team member. An administrator wants an activity report for the last 30 days.&lt;/p&gt;

&lt;p&gt;Each operation has a different access pattern.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F68esnbbo5hi5yv5z8erv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F68esnbbo5hi5yv5z8erv.png" alt=" " width="619" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These operations matter because they describe how the database will actually be used.&lt;/p&gt;

&lt;p&gt;A schema can accurately represent every entity and relationship yet still make common operations unnecessarily expensive.&lt;/p&gt;

&lt;p&gt;The reverse is also possible. A schema designed to optimize one query might make other operations difficult or introduce data consistency problems.&lt;/p&gt;

&lt;p&gt;The objective isn’t to make every query equally fast. That’s rarely realistic.&lt;/p&gt;

&lt;p&gt;It’s to understand which operations matter most, what they require, and how the data model can support them without creating unacceptable trade-offs elsewhere.&lt;/p&gt;

&lt;p&gt;Your entities describe the domain. Your access patterns reveal the demands your database must satisfy.&lt;/p&gt;

&lt;p&gt;Both are necessary, but they answer different questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the product’s questions
&lt;/h2&gt;

&lt;p&gt;An access pattern is a recurring way the application reads or changes data to support a user journey, business rule, or background task.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find a user by email during login.&lt;/li&gt;
&lt;li&gt;Load a tenant’s active projects, newest first.&lt;/li&gt;
&lt;li&gt;Find tasks assigned to a user and sort them by due date.&lt;/li&gt;
&lt;li&gt;Retrieve an order and its line items.&lt;/li&gt;
&lt;li&gt;Check whether a payment webhook has already been processed.&lt;/li&gt;
&lt;li&gt;Find subscriptions due for renewal.&lt;/li&gt;
&lt;li&gt;Count unread notifications.&lt;/li&gt;
&lt;li&gt;List events for a device within a time range.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not details to postpone until performance becomes a problem. They are part of the system’s requirements.&lt;/p&gt;

&lt;p&gt;For each important access pattern, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What information does the application have when it makes this query?&lt;/li&gt;
&lt;li&gt;What data must it return?&lt;/li&gt;
&lt;li&gt;How often will it run?&lt;/li&gt;
&lt;li&gt;How many records might it touch?&lt;/li&gt;
&lt;li&gt;How quickly must it respond?&lt;/li&gt;
&lt;li&gt;Must the result reflect the latest write?&lt;/li&gt;
&lt;li&gt;What should happen if the operation is repeated?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those answers shape the data model, indexes, constraints, and sometimes the database itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Around Real Queries, Not Imaginary Future Requirements
&lt;/h2&gt;

&lt;p&gt;Before creating tables, write down the operations the application must support.&lt;/p&gt;

&lt;p&gt;Not every hypothetical feature you might build someday. Start with the operations that are necessary for the current product.&lt;/p&gt;

&lt;p&gt;Suppose one of the most common operations in our project management application is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Show the 20 most recently updated tasks in a project, including their titles, statuses, and assignees.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence already tells us a lot.&lt;/p&gt;

&lt;p&gt;We need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restrict the results to one project.&lt;/li&gt;
&lt;li&gt;Order tasks by their last update.&lt;/li&gt;
&lt;li&gt;Return a limited number of records.&lt;/li&gt;
&lt;li&gt;Retrieve the fields needed by the interface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Include assignee information without creating unnecessary database round trips.&lt;/p&gt;

&lt;p&gt;We can express the operation in SQL:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh99q0tz7hvx7icq6byx5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh99q0tz7hvx7icq6byx5.png" alt=" " width="800" height="741"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice what we did before creating an index or changing the schema.&lt;/p&gt;

&lt;p&gt;We identified the actual query.&lt;/p&gt;

&lt;p&gt;From this query, we can begin making informed decisions about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which columns need to exist.&lt;/li&gt;
&lt;li&gt;Which relationships must be represented.&lt;/li&gt;
&lt;li&gt;Which fields are used for filtering and sorting.&lt;/li&gt;
&lt;li&gt;Whether an index could support the operation.&lt;/li&gt;
&lt;li&gt;Which fields the application actually needs to retrieve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The query also reveals an important detail: updated_at alone may not provide a deterministic order when multiple tasks have the same timestamp.&lt;/p&gt;

&lt;p&gt;Adding id as a tie-breaker gives us a stable ordering for pagination.&lt;/p&gt;

&lt;p&gt;This isn’t an argument for writing every SQL query before designing every table. It’s an argument for understanding the operations the schema is supposed to support.&lt;/p&gt;

&lt;p&gt;You can still use entity diagrams, domain models, and normalization techniques. The difference is that you validate those designs against real access requirements rather than assuming the model will automatically produce efficient queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build an access-pattern inventory
&lt;/h3&gt;

&lt;p&gt;For a real project, I would document the important operations in a small table:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fes84healbjrnl8yp8ps9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fes84healbjrnl8yp8ps9.png" alt=" " width="614" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These ratings are illustrative, not universal. A financial report may require stricter consistency than a project dashboard, while a dashboard may have tighter latency requirements.&lt;/p&gt;

&lt;p&gt;The point is to make the trade-offs explicit.&lt;/p&gt;

&lt;p&gt;A frequently executed query deserves more attention than an administrative operation run once a month, unless that less frequent operation has unusually high business impact.&lt;/p&gt;

&lt;p&gt;Database design becomes more deliberate when every important part of the schema can be connected to a real requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Access Patterns Should Influence Your Indexes
&lt;/h2&gt;

&lt;p&gt;Indexes are one of the clearest examples of why access patterns matter.&lt;/p&gt;

&lt;p&gt;Developers sometimes create indexes because a column feels important, because it’s frequently mentioned in the application, or because they expect the database to become slow eventually.&lt;/p&gt;

&lt;p&gt;But an index is useful when it helps the database execute a workload more efficiently.&lt;/p&gt;

&lt;p&gt;Let’s return to our task query:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fek80b9u0xt5y87nsk2u2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fek80b9u0xt5y87nsk2u2.png" alt=" " width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A potential index is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ntxxiv0wk7wowrvaw98.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ntxxiv0wk7wowrvaw98.png" alt=" " width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why these columns, and why this order?&lt;/p&gt;

&lt;p&gt;The query first restricts rows to a particular project_id. Within that project, it needs tasks ordered by updated_at and then id.&lt;/p&gt;

&lt;p&gt;The composite index reflects that sequence.&lt;/p&gt;

&lt;p&gt;PostgreSQL may be able to use the index to locate the project’s tasks in the required order and stop after retrieving enough matching rows.&lt;/p&gt;

&lt;p&gt;The actual execution plan still depends on the table’s size, data distribution, selectivity, and other factors. An index doesn’t guarantee that PostgreSQL will choose it.&lt;/p&gt;

&lt;p&gt;But we have a reason for creating it: it supports a known access pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why column order matters
&lt;/h2&gt;

&lt;p&gt;Consider this alternative:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxl1ciu9h6bugm9gdqe2t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxl1ciu9h6bugm9gdqe2t.png" alt=" " width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This index has the same columns, but a different leading column.&lt;/p&gt;

&lt;p&gt;It may be useful for queries that primarily scan tasks in update-time order. However, it is generally less suitable for efficiently locating one project’s range of tasks in the same way as the first index.&lt;/p&gt;

&lt;p&gt;For a B-tree index, leading columns matter because they determine how effectively the index can narrow the search.&lt;/p&gt;

&lt;p&gt;That doesn’t mean every index must start with the most frequently mentioned column. It means the index must be evaluated against the predicates and ordering of the queries you need to support.&lt;/p&gt;

&lt;p&gt;Now consider the cost.&lt;/p&gt;

&lt;p&gt;Every additional index consumes storage. Inserts and deletes must maintain it, and updates to indexed columns can require additional index work.&lt;/p&gt;

&lt;p&gt;An index can make one query faster while making write operations more expensive.&lt;/p&gt;

&lt;p&gt;This is why blindly indexing every filterable field isn’t a good strategy.&lt;/p&gt;

&lt;p&gt;A better process is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identify an important query.&lt;/li&gt;
&lt;li&gt;Inspect its execution plan with EXPLAIN.&lt;/li&gt;
&lt;li&gt;Determine whether scanning, sorting, joining, or another operation is expensive.&lt;/li&gt;
&lt;li&gt;Design an index that targets the actual bottleneck.&lt;/li&gt;
&lt;li&gt;Measure the result using realistic data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjto86l3p9etam9lm94ua.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjto86l3p9etam9lm94ua.png" alt=" " width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;EXPLAIN ANALYZE executes the query and reports actual execution details, while BUFFERS provides information about data access through PostgreSQL’s buffer system.&lt;/p&gt;

&lt;p&gt;Be careful when running it against production queries that have side effects: EXPLAIN ANALYZE executes the statement. For mutating statements, use an appropriately controlled environment or a safe transaction strategy.&lt;/p&gt;

&lt;p&gt;Also remember that a query plan measured on a tiny development database may not predict its behaviour against millions of production rows.&lt;/p&gt;

&lt;p&gt;Indexes should be a response to observed or well-understood access patterns, not a substitute for understanding them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same Data Can Need Different Models for Different Access Patterns
&lt;/h2&gt;

&lt;p&gt;Here’s where database design becomes more interesting.&lt;/p&gt;

&lt;p&gt;Suppose our application stores workspace activity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TaskCreated&lt;/li&gt;
&lt;li&gt;TaskAssigned&lt;/li&gt;
&lt;li&gt;TaskCompleted&lt;/li&gt;
&lt;li&gt;CommentAdded&lt;/li&gt;
&lt;li&gt;MemberInvited&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each event records what happened, when it happened, and which workspace it belongs to.&lt;/p&gt;

&lt;p&gt;A natural table might look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8twtqm6e9hxftk4i88ls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8twtqm6e9hxftk4i88ls.png" alt=" " width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a reasonable starting point for a workspace activity feed.&lt;/p&gt;

&lt;p&gt;But what if the product introduces several different requirements?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Show the latest 50 events in a workspace.&lt;/li&gt;
&lt;li&gt;Retrieve the history of one particular task.&lt;/li&gt;
&lt;li&gt;Count completed tasks per day.&lt;/li&gt;
&lt;li&gt;Search activity by actor.&lt;/li&gt;
&lt;li&gt;Generate a monthly report grouped by event type.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are different workloads against the same underlying information.&lt;/p&gt;

&lt;p&gt;The workspace feed is optimized around a workspace and time range. Task history is centered on an entity. Reporting requires aggregation. Search may require text indexing or a separate search system if the requirements grow sufficiently complex.&lt;br&gt;
One table and one index will not necessarily serve every workload optimally.&lt;/p&gt;

&lt;p&gt;This doesn’t mean you immediately need multiple databases or duplicated data. It means you should recognize that a data model is a set of trade-offs across multiple operations.&lt;/p&gt;

&lt;p&gt;You might add another index if task-history queries become important:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F691hwh89kbnshdukej7g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F691hwh89kbnshdukej7g.png" alt=" " width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If reporting becomes expensive, you might introduce a summary table or a materialized view. If text search becomes a major workload, you might evaluate PostgreSQL’s full-text search before introducing a dedicated search engine.&lt;/p&gt;

&lt;p&gt;The appropriate response depends on query frequency, latency requirements, data volume, and the freshness guarantees the product needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about denormalization?
&lt;/h3&gt;

&lt;p&gt;Sometimes you deliberately store information in more than one place because the read workload justifies it.&lt;/p&gt;

&lt;p&gt;Imagine a dashboard that displays the total number of open tasks in each project.&lt;/p&gt;

&lt;p&gt;One option is to calculate the count every time the dashboard loads:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhvr82pwg0y0e4c4hsgzc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhvr82pwg0y0e4c4hsgzc.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a small application, this may be perfectly adequate.&lt;/p&gt;

&lt;p&gt;But if the task table grows significantly and this aggregation becomes a frequent, expensive operation, you might maintain a project-level counter or a summary table.&lt;/p&gt;

&lt;p&gt;Now the dashboard can retrieve a precomputed value rather than repeatedly aggregating the entire matching dataset.&lt;/p&gt;

&lt;p&gt;The trade-off is that the summary must remain correct.&lt;/p&gt;

&lt;p&gt;If a task changes from OPEN to DONE, the count must be updated. If a transaction fails, the counter must not drift away from the underlying data. If events are processed asynchronously, the summary may temporarily lag behind the source records.&lt;/p&gt;

&lt;p&gt;A denormalized model is not simply a faster version of a normalized model. It exchanges some computation at read time for additional complexity in maintaining the data.&lt;/p&gt;

&lt;p&gt;This is a decision to make when you understand the workload and can justify the cost.&lt;/p&gt;

&lt;p&gt;Normalize to protect the integrity of your data. Denormalize when a demonstrated access requirement justifies the additional consistency work.&lt;/p&gt;

&lt;p&gt;Don’t duplicate data merely because you assume reads will eventually become slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  ORMs Don’t Remove the Need to Think About Queries
&lt;/h2&gt;

&lt;p&gt;Working with an ORM makes database access more convenient. It doesn’t make the underlying query patterns disappear.&lt;/p&gt;

&lt;p&gt;Consider a NestJS service using Prisma:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjbwz3go12tazfbsxersz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjbwz3go12tazfbsxersz.png" alt=" " width="800" height="790"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a reasonable implementation of the query we discussed earlier.&lt;/p&gt;

&lt;p&gt;It filters by project, orders the results deterministically, limits the result set, and retrieves only the assignee fields needed by the application.&lt;/p&gt;

&lt;p&gt;But an ORM call isn’t automatically efficient just because it looks clean.&lt;/p&gt;

&lt;p&gt;Consider another implementation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fam46i5wsle232zp45hmm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fam46i5wsle232zp45hmm.png" alt=" " width="800" height="710"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This pattern can produce an additional query for each task with an assignee.&lt;/p&gt;

&lt;p&gt;If the initial query returns 20 tasks, the service could issue up to 21 queries instead of retrieving the necessary data through a more efficient access strategy.&lt;/p&gt;

&lt;p&gt;This is a version of the N+1 query problem.&lt;/p&gt;

&lt;p&gt;The issue becomes more significant when the pattern appears inside nested loops, list endpoints, or other high-traffic operations.&lt;/p&gt;

&lt;p&gt;ORMs provide tools for loading relations, selecting fields, batching queries, and managing transactions. The exact SQL generated depends on the ORM, its configuration, and the query being executed.&lt;/p&gt;

&lt;p&gt;The important thing is to understand what happens beneath the abstraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical ORM workflow
&lt;/h2&gt;

&lt;p&gt;When designing an endpoint, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which records does this operation actually need?&lt;/li&gt;
&lt;li&gt;Which fields should be returned?&lt;/li&gt;
&lt;li&gt;Which relationships must be loaded?&lt;/li&gt;
&lt;li&gt;Can the data be retrieved in a single efficient query, or is a small number of targeted queries preferable?&lt;/li&gt;
&lt;li&gt;Will this operation trigger repeated queries for every returned record?&lt;/li&gt;
&lt;li&gt;Does the query have an appropriate index?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should also inspect the SQL generated by the ORM when performance matters.&lt;/p&gt;

&lt;p&gt;Abstraction is useful when it makes code easier to maintain. It becomes a problem when it hides a costly access pattern that nobody is monitoring.&lt;/p&gt;

&lt;p&gt;An ORM can simplify how you express a query. It cannot make a poorly designed query free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pagination Is a Database Design Problem, Too
&lt;/h2&gt;

&lt;p&gt;Pagination looks like an API concern, but the way you paginate data can have a significant impact on database performance.&lt;/p&gt;

&lt;p&gt;Imagine the application has hundreds of thousands of tasks in a workspace.&lt;/p&gt;

&lt;p&gt;A common approach is offset pagination:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9odrp034dd7z2etlfwf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9odrp034dd7z2etlfwf.png" alt=" " width="799" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This asks the database to skip the first 10,000 rows in the ordered result and return the next 20.&lt;/p&gt;

&lt;p&gt;Offset pagination is straightforward and works well for many applications. But as the offset grows, the database may need to walk through an increasingly large number of index entries or rows before it can return the requested page.&lt;/p&gt;

&lt;p&gt;Large offsets can therefore become expensive.&lt;/p&gt;

&lt;p&gt;There is another concern: if records are inserted, deleted, or reordered between requests, users may see duplicate or missing items as they navigate through pages.&lt;/p&gt;

&lt;p&gt;For long lists, feeds, and other workloads where users move forward through a changing dataset, keyset pagination can be a better fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use the last record as a cursor
&lt;/h3&gt;

&lt;p&gt;Suppose the previous page ended with a task whose updated_at was 2026-10-10 10:00:00 and whose ID was task_123.&lt;/p&gt;

&lt;p&gt;The next page can request records after that position in the descending order:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3kn07beoxuh2ba7uyqzu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3kn07beoxuh2ba7uyqzu.png" alt=" " width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, $2 is the timestamp of the last record from the previous page, and $3 is its ID.&lt;/p&gt;

&lt;p&gt;Because both ordering columns are descending, the row comparison selects records that come after the cursor in that ordering, assuming the columns are non-null and the cursor values use the same ordering semantics.&lt;/p&gt;

&lt;p&gt;The composite index from earlier can support this access pattern:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fogu4407vtiytpsydm1pf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fogu4407vtiytpsydm1pf.png" alt=" " width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This lets the database seek into the ordered index near the cursor rather than repeatedly skipping a large offset.&lt;/p&gt;

&lt;p&gt;There are still important details to consider.&lt;/p&gt;

&lt;p&gt;The ordering columns should form a deterministic order, the cursor should encode the relevant values reliably, and nullable columns require additional care. If updated_at changes while a task is being paginated, that task can move relative to the cursor. Keyset pagination reduces some problems associated with offset pagination, but it does not automatically provide a consistent snapshot across multiple HTTP requests.&lt;/p&gt;

&lt;p&gt;For a feed where stable traversal is important, you may need an immutable ordering key, a snapshot boundary, or a different consistency strategy.&lt;/p&gt;

&lt;p&gt;Also, if users need to jump directly to page 500, offset pagination may be more convenient.&lt;/p&gt;

&lt;p&gt;The right choice depends on the product.&lt;/p&gt;

&lt;p&gt;The important lesson is that pagination isn’t just about returning a subset of records. It’s about how the database finds that subset, how records are ordered, and what happens when the dataset changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access Patterns Must Not Override Data Integrity
&lt;/h2&gt;

&lt;p&gt;At this point, it might sound as if every table should be designed around the fastest possible query.&lt;/p&gt;

&lt;p&gt;That would be another mistake.&lt;/p&gt;

&lt;p&gt;Access patterns are a starting point for understanding the workload, not permission to ignore the integrity of the underlying data.&lt;/p&gt;

&lt;p&gt;Suppose our application stores a task’s status in a tasks table and also maintains a separate summary table containing the number of open tasks per project.&lt;/p&gt;

&lt;p&gt;The summary makes dashboard reads cheaper.&lt;/p&gt;

&lt;p&gt;But now a task changes from OPEN to DONE.&lt;/p&gt;

&lt;p&gt;If the task update succeeds and the summary update fails, the two representations disagree.&lt;/p&gt;

&lt;p&gt;You have improved the read path but introduced a correctness problem.&lt;/p&gt;

&lt;p&gt;There are several ways to address this, depending on the system.&lt;/p&gt;

&lt;p&gt;For a small summary maintained inside PostgreSQL, you may update the task and the counter within the same transaction. For an asynchronously maintained projection, you may use reliable event processing, idempotent handlers, and reconciliation jobs to detect or repair inconsistencies.&lt;/p&gt;

&lt;p&gt;These are different consistency models, with different operational costs.&lt;/p&gt;

&lt;p&gt;The same principle applies to normalization.&lt;/p&gt;

&lt;p&gt;If a customer’s address is stored in several tables, changing the address may require updating multiple records. If those copies are intended to represent the same current value, you need a strategy for keeping them consistent.&lt;/p&gt;

&lt;p&gt;But not every duplicated value is a mistake.&lt;/p&gt;

&lt;p&gt;An order item often stores the price agreed upon when the order was placed, even though the current product price may change later. That value is a historical snapshot, not an accidental duplicate.&lt;/p&gt;

&lt;p&gt;Understanding the business meaning of data is just as important as understanding how it is queried.&lt;/p&gt;

&lt;p&gt;Before denormalizing, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this value authoritative or derived?&lt;/li&gt;
&lt;li&gt;Can it change independently of its source?&lt;/li&gt;
&lt;li&gt;How fresh must the derived value be?&lt;/li&gt;
&lt;li&gt;What happens if an update or event is processed twice?&lt;/li&gt;
&lt;li&gt;Can the application recover from a failed update?&lt;/li&gt;
&lt;li&gt;How will the system detect inconsistent data?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fast query is not an improvement if it makes the application return incorrect answers.&lt;/p&gt;

&lt;p&gt;The goal is not to optimize access patterns at any cost. It’s to balance efficient data access with the correctness guarantees the application requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Workflow for Access-Pattern-Driven Design
&lt;/h2&gt;

&lt;p&gt;You don’t need a complicated process to apply these ideas. You need a deliberate one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Identify the core domain entities
&lt;/h3&gt;

&lt;p&gt;Start with the things the application needs to represent: users, orders, products, workspaces, tasks, payments, or whatever makes sense for your domain.&lt;/p&gt;

&lt;p&gt;Define their relationships and the business rules that must hold.&lt;/p&gt;

&lt;p&gt;This gives you the foundation of the model.&lt;/p&gt;

&lt;p&gt;Here’s a workflow you can use when designing a backend database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: List the important application operations
&lt;/h3&gt;

&lt;p&gt;Write down the reads and writes behind the major user journeys and background jobs.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load a customer’s recent orders.&lt;/li&gt;
&lt;li&gt;Retrieve the items in an order.&lt;/li&gt;
&lt;li&gt;Reserve inventory.&lt;/li&gt;
&lt;li&gt;Find overdue tasks for a project.&lt;/li&gt;
&lt;li&gt;Display a workspace activity feed.&lt;/li&gt;
&lt;li&gt;Calculate monthly subscription usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid vague requirements such as “the system needs to retrieve tasks.” Specify the filters, ordering, limits, and related data that the operation requires.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Turn important operations into queries
&lt;/h3&gt;

&lt;p&gt;Express the operations in SQL or in the ORM you use.&lt;/p&gt;

&lt;p&gt;You don’t need to implement every query completely at the beginning. But you should understand what the database will need to do.&lt;/p&gt;

&lt;p&gt;For reads, consider filtering, sorting, joins, aggregation, and pagination.&lt;/p&gt;

&lt;p&gt;For writes, consider constraints, atomic updates, transaction boundaries, and concurrent operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Design the schema and constraints
&lt;/h3&gt;

&lt;p&gt;Now create the tables, relationships, data types, foreign keys, unique constraints, and other rules that reflect the domain.&lt;/p&gt;

&lt;p&gt;Use normalization where it helps prevent unwanted duplication and update anomalies.&lt;/p&gt;

&lt;p&gt;Where a particular access pattern calls for a different representation, evaluate the trade-offs deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Add indexes based on query requirements
&lt;/h3&gt;

&lt;p&gt;Review the most important queries and identify which indexes could support them.&lt;/p&gt;

&lt;p&gt;Pay particular attention to composite indexes, column order, filtering, sorting, and pagination.&lt;/p&gt;

&lt;p&gt;Don’t forget that indexes have write and storage costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Test with realistic data
&lt;/h3&gt;

&lt;p&gt;A query that works quickly against 100 rows may behave very differently against 5 million.&lt;/p&gt;

&lt;p&gt;Populate your database with representative data volumes and distributions. Run EXPLAIN ANALYZE, inspect buffer usage, and compare query behavior before and after changes.&lt;/p&gt;

&lt;p&gt;Test concurrent writes when the application relies on inventory reservations, financial operations, uniqueness, or other important invariants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Measure, revise, and document
&lt;/h3&gt;

&lt;p&gt;Use query logs, application metrics, and database monitoring to understand what happens in production.&lt;/p&gt;

&lt;p&gt;If the workload changes, revisit the design.&lt;/p&gt;

&lt;p&gt;Perhaps a new dashboard makes a previously unimportant query critical. Perhaps a summary table is now justified. Perhaps the existing index is sufficient, and the real problem lies in connection management or repeated application queries.&lt;/p&gt;

&lt;p&gt;Document the reason behind important design decisions so the next engineer understands what the schema is optimized for and which trade-offs it makes.&lt;/p&gt;

&lt;p&gt;Database design is not a one-time exercise. It evolves as the application’s workload evolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of designing without access patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Slow queries
&lt;/h3&gt;

&lt;p&gt;A query may require scanning millions of rows because no suitable index exists.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcuiph3aidemq59m4vane.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcuiph3aidemq59m4vane.png" alt=" " width="722" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without a suitable index, the database may scan many rows to find matching orders.&lt;/p&gt;

&lt;p&gt;PostgreSQL’s query planner chooses between scan strategies such as sequential scans, index scans, and bitmap index scans based on the query structure and data properties.&lt;/p&gt;

&lt;p&gt;The right plan depends on the query and the data.&lt;/p&gt;

&lt;p&gt;If you do not understand the query, you cannot design the right index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Too many indexes
&lt;/h3&gt;

&lt;p&gt;Adding an index to every column may seem safe, but indexes are not free.&lt;/p&gt;

&lt;p&gt;They consume storage and must be maintained during inserts, updates, and deletes. PostgreSQL documentation explicitly notes that indexes improve retrieval speed but add overhead to the database system as a whole.&lt;/p&gt;

&lt;p&gt;A schema with too many indexes can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow down writes.&lt;/li&gt;
&lt;li&gt;Increase storage cost.&lt;/li&gt;
&lt;li&gt;Complicate migrations.&lt;/li&gt;
&lt;li&gt;Confuse the query planner.&lt;/li&gt;
&lt;li&gt;Create maintenance burden.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Design indexes around measured access patterns, not around every possible query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expensive joins
&lt;/h3&gt;

&lt;p&gt;If two pieces of data are always read together, storing them separately may create unnecessary joins.&lt;/p&gt;

&lt;p&gt;For example, if every product page needs product details and its primary image, a design that requires a join for every page load may be less efficient than a design that stores or projects the image reference alongside the product.&lt;/p&gt;

&lt;p&gt;But this does not mean you should duplicate everything.&lt;/p&gt;

&lt;p&gt;Denormalization should be deliberate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data integrity problems
&lt;/h3&gt;

&lt;p&gt;Access patterns also reveal constraints.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A user should have only one active subscription.&lt;/li&gt;
&lt;li&gt;A backup code should be used only once.&lt;/li&gt;
&lt;li&gt;A payment reference should be unique.&lt;/li&gt;
&lt;li&gt;A task must belong to a project.&lt;/li&gt;
&lt;li&gt;An invoice item must reference an invoice.&lt;/li&gt;
&lt;li&gt;A tenant-owned record must include a tenant ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These rules should be represented in the schema, not only in application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts: Don’t Just Model Your Data. Model How You’ll Use It.
&lt;/h2&gt;

&lt;p&gt;A well-designed database isn’t simply one with clean tables, sensible relationships, and a handful of indexes.&lt;/p&gt;

&lt;p&gt;It’s one whose structure supports the operations the application needs to perform while preserving the integrity of its data.&lt;/p&gt;

&lt;p&gt;That’s why starting with entities alone isn’t enough.&lt;/p&gt;

&lt;p&gt;You need to understand the reads, writes, filters, sorts, joins, transactions, and reports that will shape the database’s workload. Those operations reveal where indexes matter, where normalization helps, where denormalization might be justified, and where an apparently simple endpoint could become an expensive production query.&lt;/p&gt;

&lt;p&gt;You don’t need to optimize for every imaginable future requirement. You don’t need to introduce specialized databases before you have a reason. And you certainly don’t need to sacrifice correctness for a benchmark.&lt;/p&gt;

&lt;p&gt;Start with the domain. Understand the access patterns. Design the schema to support both. Then measure the result and let real workload evidence guide your next decision.&lt;/p&gt;

&lt;p&gt;The best database schema isn’t the one that looks the cleanest on a diagram. It’s the one that represents your domain correctly and supports the way your application actually works.&lt;/p&gt;

&lt;p&gt;Design for the queries your system needs, not just the tables it contains.&lt;/p&gt;

</description>
      <category>backenddevelopment</category>
      <category>databasedesign</category>
      <category>systemdesign</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Your Database Is an Architectural Decision. Not Just Storage.</title>
      <dc:creator>Nsikan Patrick Adaowo</dc:creator>
      <pubDate>Sat, 10 Oct 2026 14:15:06 +0000</pubDate>
      <link>https://dev.to/nsikanadaowo/your-database-is-an-architectural-decision-not-just-storage-2m6c</link>
      <guid>https://dev.to/nsikanadaowo/your-database-is-an-architectural-decision-not-just-storage-2m6c</guid>
      <description>&lt;p&gt;When developers start a new project, the database is often treated as an implementation detail.&lt;/p&gt;

&lt;p&gt;The conversation sounds simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should we use PostgreSQL or MongoDB?&lt;/li&gt;
&lt;li&gt;Should we add Redis?&lt;/li&gt;
&lt;li&gt;Which database scales better?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six months later.&lt;/p&gt;

&lt;p&gt;The product gains traction. Traffic hits 2,000 requests per minute. Suddenly, P99 latency spikes to 4 seconds. Your connection pool exhausts every time a scheduled reporting job runs. A single JOIN on a multi-million-row table locks up CPU cores, and your database memory charts skyrocket toward a production crash.&lt;/p&gt;

&lt;p&gt;In a panic, the team throws a Redis cluster in front of the database to “cache the slow queries.” When that doesn’t fix the underlying write locks, someone suggests migrating to MongoDB because “Document stores scale better.”&lt;/p&gt;

&lt;p&gt;This is the exact moment you realize a painful truth: You didn’t just pick a place to store data. You made an architectural decision that defined your system’s throughput limits, concurrency mechanics, scaling strategy and failure modes—and you did it without thinking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnywy2i9ht24nyb2ayqh8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnywy2i9ht24nyb2ayqh8.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A Relatable example
&lt;/h3&gt;

&lt;p&gt;Consider a simple e-commerce application.&lt;/p&gt;

&lt;p&gt;A customer places an order. The application needs to create the order, record its items, reserve inventory, and initiate payment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now ask a few questions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens if the order is created but inventory reservation fails?&lt;/li&gt;
&lt;li&gt;How do you prevent two customers from purchasing the last available item?&lt;/li&gt;
&lt;li&gt;What happens when the product catalogue grows from 1,000 products to 10 million?&lt;/li&gt;
&lt;li&gt;How efficiently can you retrieve an order with its items, payment status, and delivery information?&lt;/li&gt;
&lt;li&gt;What happens when a query that worked perfectly in development becomes one of your most expensive production operations?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not just database questions. They are architectural questions.&lt;/p&gt;

&lt;p&gt;And the database you choose determines which of them are straightforward, which require additional engineering, and which become significantly harder to get right.&lt;/p&gt;

&lt;p&gt;Let’s look at why.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Your Data Model Is Also a Design Decision
&lt;/h3&gt;

&lt;p&gt;One of the first mistakes developers make is choosing a database based on how the data looks rather than how the application needs to use it.&lt;/p&gt;

&lt;p&gt;Imagine a product in an e-commerce application:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmjluaxv0n1zo1sm1dok7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmjluaxv0n1zo1sm1dok7.png" alt=" " width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At first glance, this looks like a simple document. You could store it in a relational database or a document database.&lt;/p&gt;

&lt;p&gt;Both can represent it.&lt;/p&gt;

&lt;p&gt;But the important question isn’t whether a database can store your data.&lt;/p&gt;

&lt;p&gt;It’s how the data relates to everything else in your system.&lt;/p&gt;

&lt;p&gt;A product belongs to a category. An order contains products. A customer places orders. A payment belongs to an order. Inventory changes as orders are placed and cancelled.&lt;/p&gt;

&lt;p&gt;The data is connected, and those connections carry business meaning.&lt;/p&gt;

&lt;p&gt;In a relational database, you might model the relationships explicitly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdqthp2ndl0nzuu79qnxp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdqthp2ndl0nzuu79qnxp.png" alt=" " width="800" height="1127"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This structure makes several decisions explicit.&lt;/p&gt;

&lt;p&gt;Products reference categories. Order items reference products. Prices and quantities have defined types. Foreign keys can prevent references to non-existent records.&lt;/p&gt;

&lt;p&gt;The schema becomes part of the system’s correctness model.&lt;/p&gt;

&lt;p&gt;A document database can also represent these relationships, but the design might look different. You may embed data that is usually retrieved together, reference data that changes independently, or maintain denormalized copies for specific queries.&lt;/p&gt;

&lt;p&gt;Neither approach is automatically superior.&lt;/p&gt;

&lt;p&gt;The difference is where you place the complexity.&lt;/p&gt;

&lt;p&gt;With relational modeling, you often manage relationships through joins, constraints, and transactions. With document-oriented modeling, you may simplify particular reads but take on duplication, update coordination, or additional application logic.&lt;/p&gt;

&lt;p&gt;The architectural question here is not which database can represent your data. It’s which data model best supports the relationships and operations your application must guarantee.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Start With Access Patterns, Not Database Popularity
&lt;/h3&gt;

&lt;p&gt;Suppose you’re building a SaaS application.&lt;/p&gt;

&lt;p&gt;You know you need to store users, projects, tasks, and activity logs.&lt;/p&gt;

&lt;p&gt;That tells you what data exists. It doesn’t tell you how the system will access it.&lt;/p&gt;

&lt;p&gt;Consider these operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieve a project and its most recent tasks.&lt;/li&gt;
&lt;li&gt;Find all tasks assigned to a user across multiple projects.&lt;/li&gt;
&lt;li&gt;Display the latest 50 activity events for a workspace.&lt;/li&gt;
&lt;li&gt;Calculate monthly usage for billing.&lt;/li&gt;
&lt;li&gt;Search projects by name within a particular organization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These queries have different requirements.&lt;/p&gt;

&lt;p&gt;Some filter by relationships. Others sort by time. Some need aggregations. Others require low-latency lookups.&lt;/p&gt;

&lt;p&gt;Your database schema, indexes, and even your choice of storage model should reflect these access patterns.&lt;/p&gt;

&lt;p&gt;For example, if your application frequently retrieves recent activity for a workspace, you might query PostgreSQL like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftnw1p3x8k4ymfo3hlqv1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftnw1p3x8k4ymfo3hlqv1.png" alt=" " width="799" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A suitable index could be:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fls103k7zd85h93fnv2vb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fls103k7zd85h93fnv2vb.png" alt=" " width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this index?&lt;/strong&gt;&lt;br&gt;
Because the query filters by workspace_id, sorts by created_at, and returns only a limited number of rows.&lt;/p&gt;

&lt;p&gt;The index can help PostgreSQL locate the relevant workspace records and retrieve recent entries without sorting the entire matching dataset.&lt;/p&gt;

&lt;p&gt;The exact benefit depends on the table size, data distribution, query plan, and workload. An index is not a guarantee of a faster query in every situation.&lt;/p&gt;

&lt;p&gt;It also introduces costs: additional storage and work during inserts, updates, and deletes.&lt;/p&gt;

&lt;p&gt;That is the trade-off.&lt;/p&gt;

&lt;p&gt;Now imagine discovering this access pattern after the application is already in production. You may need to change indexes, restructure queries, or redesign the way activity data is stored.&lt;/p&gt;

&lt;p&gt;None of that is impossible. But understanding the workload earlier can prevent avoidable complexity.&lt;/p&gt;

&lt;p&gt;This is why database design should begin with questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What are the most frequent reads and writes?&lt;/li&gt;
&lt;li&gt;Which fields are used for filtering and sorting?&lt;/li&gt;
&lt;li&gt;Which relationships must be traversed?&lt;/li&gt;
&lt;li&gt;Which queries must remain fast as the dataset grows?&lt;/li&gt;
&lt;li&gt;Which operations require aggregation or reporting?&lt;/li&gt;
&lt;li&gt;What latency can the application tolerate?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don’t choose a database because it fits your data. Choose a database architecture that fits the way your system uses that data.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Transactions Are Where Database Choices Become Business Decisions
&lt;/h3&gt;

&lt;p&gt;Some operations can tolerate partial completion.&lt;/p&gt;

&lt;p&gt;Others cannot.&lt;/p&gt;

&lt;p&gt;If a user updates their profile picture and the notification email fails, the system can usually retry the email independently.&lt;/p&gt;

&lt;p&gt;But if an order is recorded while its inventory reservation fails, the application may accept a purchase it cannot fulfil.&lt;/p&gt;

&lt;p&gt;Consider this simplified NestJS service using Prisma:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6aq3pg9k3tyl32sqnl1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6aq3pg9k3tyl32sqnl1.png" alt=" " width="800" height="1119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The intention is sound: group the inventory update and order creation into one transaction so that a failure rolls back the database changes.&lt;/p&gt;

&lt;p&gt;But there is a subtle concurrency problem.&lt;/p&gt;

&lt;p&gt;Imagine the product has one item remaining.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/nsikanadaowo/what-actually-happens-when-10000-users-hit-your-buy-now-button-at-the-same-time-5c18"&gt;Two requests arrive at almost the same time&lt;/a&gt;. Both read a stock value of one. Both pass the check. Both decrement the stock.&lt;/p&gt;

&lt;p&gt;Depending on the database’s isolation behaviour and the operations performed, a naive read-check-update sequence can allow overselling or produce an invalid stock value.&lt;/p&gt;

&lt;p&gt;Wrapping code in a transaction does not automatically make every business invariant safe.&lt;/p&gt;

&lt;p&gt;One way to improve this design in PostgreSQL is to perform a conditional update:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fesrpyrk7qn8sj0y1jscm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fesrpyrk7qn8sj0y1jscm.png" alt=" " width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The update only succeeds if sufficient stock remains when PostgreSQL evaluates the condition. Concurrent updates to the same row are coordinated by the database, and the predicate is checked against the row being updated.&lt;/p&gt;

&lt;p&gt;If the update count is zero, the reservation failed. Throwing an error rolls back the transaction, including any order creation performed within it.&lt;/p&gt;

&lt;p&gt;In a real system, you’d also validate that quantity is positive, enforce appropriate database constraints, and handle errors deliberately.&lt;/p&gt;

&lt;p&gt;The important distinction is that the database is doing more than persisting information. It is helping enforce a business invariant under concurrency.&lt;/p&gt;

&lt;p&gt;This is where database capabilities matter.&lt;/p&gt;

&lt;p&gt;You need to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Atomicity: do related database changes succeed or fail together?&lt;/li&gt;
&lt;li&gt;Isolation: what can concurrent transactions observe?&lt;/li&gt;
&lt;li&gt;Constraints: which invalid states can the database reject?&lt;/li&gt;
&lt;li&gt;Locking: how are conflicting writes coordinated?&lt;/li&gt;
&lt;li&gt;Durability: what guarantees exist after a successful commit?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A database that supports transactions, constraints, and suitable concurrency controls can make these guarantees easier to implement.&lt;/p&gt;

&lt;p&gt;Other storage systems may offer different transaction scopes or consistency guarantees. Some can support the same business requirements, but the design and operational trade-offs may differ.&lt;/p&gt;

&lt;p&gt;If your application depends on financial correctness, inventory accuracy, or reliable state transitions, transaction semantics are not a minor implementation detail. They’re part of your architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Your Database Choice Changes How You Scale
&lt;/h3&gt;

&lt;p&gt;“Will it scale?” is one of the most common questions developers ask about a database.&lt;/p&gt;

&lt;p&gt;But scale is not a single problem.&lt;/p&gt;

&lt;p&gt;A system may struggle because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too many concurrent connections.&lt;/li&gt;
&lt;li&gt;Expensive joins or poorly designed queries.&lt;/li&gt;
&lt;li&gt;Missing or ineffective indexes.&lt;/li&gt;
&lt;li&gt;Large write volumes.&lt;/li&gt;
&lt;li&gt;Contention on frequently updated rows.&lt;/li&gt;
&lt;li&gt;Growing storage requirements.&lt;/li&gt;
&lt;li&gt;Read traffic that exceeds the primary database’s capacity.&lt;/li&gt;
&lt;li&gt;Reporting workloads competing with transactional operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different problems require different solutions.&lt;/p&gt;

&lt;p&gt;Let’s say your SaaS application starts with a few hundred users and eventually serves tens of thousands.&lt;/p&gt;

&lt;p&gt;Your first instinct might be to migrate to a distributed database.&lt;/p&gt;

&lt;p&gt;But what if the actual problem is an unindexed query that scans millions of activity records?&lt;/p&gt;

&lt;p&gt;What if your ORM is generating dozens of queries per request?&lt;/p&gt;

&lt;p&gt;What if your application opens too many database connections?&lt;/p&gt;

&lt;p&gt;Changing database engines may introduce more complexity without addressing the bottleneck.&lt;/p&gt;

&lt;p&gt;A more sensible progression is to measure the workload, inspect query plans, improve access patterns, and identify the actual constraint.&lt;/p&gt;

&lt;p&gt;Depending on the evidence, you might introduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better indexes and query optimization.&lt;/li&gt;
&lt;li&gt;Connection pooling.&lt;/li&gt;
&lt;li&gt;Caching for frequently accessed data.&lt;/li&gt;
&lt;li&gt;Read replicas for appropriate read-heavy workloads.&lt;/li&gt;
&lt;li&gt;Partitioning for large tables with suitable access patterns.&lt;/li&gt;
&lt;li&gt;Background processing for expensive non-interactive work.&lt;/li&gt;
&lt;li&gt;Sharding or a different storage architecture when simpler options are insufficient.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each option introduces trade-offs.&lt;/p&gt;

&lt;p&gt;A read replica can distribute reads, but replication lag may mean a user doesn’t immediately see a recent write when reading from the replica.&lt;/p&gt;

&lt;p&gt;Caching can reduce database load, but it introduces invalidation and freshness problems.&lt;/p&gt;

&lt;p&gt;Partitioning can improve certain large-table workloads, but it requires careful key selection and query design.&lt;/p&gt;

&lt;p&gt;Sharding can distribute data and load, but cross-shard queries, transactions, rebalancing, and operational complexity become harder.&lt;/p&gt;

&lt;p&gt;The right decision depends on the bottleneck you’re solving.&lt;/p&gt;

&lt;p&gt;This is also why the database choice matters early: it shapes the available optimization paths, the complexity of future changes, and the skills your team needs to operate the system.&lt;/p&gt;

&lt;p&gt;However, your first database decision does not need to predict every future scaling problem.&lt;/p&gt;

&lt;p&gt;It needs to support the current workload well without making reasonable future evolution unnecessarily difficult.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. SQL vs. NoSQL Is the Wrong First Question
&lt;/h3&gt;

&lt;p&gt;The SQL-versus-NoSQL debate often starts with the wrong assumption: that one category is inherently better for modern applications.&lt;/p&gt;

&lt;p&gt;Relational databases are not outdated because document databases exist. Document databases are not automatically more scalable because their schemas are flexible.&lt;/p&gt;

&lt;p&gt;These systems optimize for different requirements, and their capabilities overlap more than simplistic comparisons suggest.&lt;/p&gt;

&lt;p&gt;A relational database such as PostgreSQL is often a strong starting point when your application has interconnected entities, important integrity constraints, complex queries, and transactional workflows.&lt;/p&gt;

&lt;p&gt;A document database can be a good fit when your data is naturally document-oriented, access patterns align with document boundaries, and flexible document structures provide a practical advantage.&lt;/p&gt;

&lt;p&gt;Key-value stores are useful when the workload primarily involves simple lookups by key and the chosen system provides the required durability and consistency guarantees.&lt;/p&gt;

&lt;p&gt;Search engines are designed for sophisticated text retrieval, relevance scoring, and search-oriented indexing.&lt;/p&gt;

&lt;p&gt;Time-series systems may be appropriate for workloads involving large volumes of timestamped measurements and time-based analysis.&lt;/p&gt;

&lt;p&gt;Notice the pattern: each choice follows the workload and its requirements.&lt;/p&gt;

&lt;p&gt;Even these categories are not mutually exclusive. PostgreSQL supports JSONB for semi-structured data, for example, and Redis supports more than basic key-value operations.&lt;/p&gt;

&lt;p&gt;You don’t have to adopt a separate database for every data type or feature.&lt;/p&gt;

&lt;p&gt;In fact, introducing several databases too early can create more problems than it solves.&lt;/p&gt;

&lt;p&gt;Now your team has multiple systems to deploy, monitor, secure, back up, restore, and understand. Data may need to move between them, and cross-system consistency can become an application-level responsibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polyglot persistence&lt;/strong&gt; — using different storage technologies for different workloads — is a legitimate architectural strategy. But it should solve a concrete problem, not serve as a demonstration of technical sophistication.&lt;/p&gt;

&lt;p&gt;For many applications, a well-designed PostgreSQL database is sufficient for transactional data, JSON-based fields, and a substantial range of query requirements.&lt;/p&gt;

&lt;p&gt;Start with the simplest architecture that satisfies the requirements. Add another database when you can explain exactly what it provides that your existing system cannot provide efficiently or safely.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Operational Complexity Is Part of the Decision
&lt;/h3&gt;

&lt;p&gt;A database is not just an engine you connect to using a connection string.&lt;/p&gt;

&lt;p&gt;It is a system you must operate.&lt;/p&gt;

&lt;p&gt;Before choosing one, consider what happens when things go wrong.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How are backups taken? &lt;/li&gt;
&lt;li&gt;Have restores been tested? &lt;/li&gt;
&lt;li&gt;How will schema migrations be deployed? &lt;/li&gt;
&lt;li&gt;How will connection limits be managed? &lt;/li&gt;
&lt;li&gt;How will slow queries be identified? &lt;/li&gt;
&lt;li&gt;What happens during failover? &lt;/li&gt;
&lt;li&gt;Can the team diagnose replication lag or storage pressure?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions are easy to ignore when an application is small.&lt;/p&gt;

&lt;p&gt;They become important when the application holds customer data, processes payments, or supports a business that cannot afford extended downtime.&lt;/p&gt;

&lt;p&gt;Consider connection pooling.&lt;/p&gt;

&lt;p&gt;If your application runs multiple NestJS instances, each creating many database connections, the combined number of connections can overwhelm the database even when query volume is reasonable.&lt;/p&gt;

&lt;p&gt;Adding more application instances may actually make the situation worse.&lt;/p&gt;

&lt;p&gt;Similarly, a migration that works on a small development database may lock a large production table or take longer than the deployment window allows.&lt;/p&gt;

&lt;p&gt;Operational requirements should therefore influence your choice.&lt;/p&gt;

&lt;p&gt;A managed PostgreSQL service might be preferable to operating a more exotic database yourself. A specialized system may offer worthwhile benefits, but those benefits must justify the extra operational burden.&lt;/p&gt;

&lt;p&gt;Evaluate the whole cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure and storage.&lt;/li&gt;
&lt;li&gt;Query performance under realistic workloads.&lt;/li&gt;
&lt;li&gt;Backups and recovery.&lt;/li&gt;
&lt;li&gt;Monitoring and debugging.&lt;/li&gt;
&lt;li&gt;Deployment and migration safety.&lt;/li&gt;
&lt;li&gt;Team familiarity and hiring.&lt;/li&gt;
&lt;li&gt;Failure recovery and availability requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cheapest database to start with is not always the cheapest system to maintain.&lt;/p&gt;

&lt;p&gt;And the most powerful database is not necessarily the best one for a team that lacks the experience to operate it reliably.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. How to Choose a Database Without Guessing
&lt;/h3&gt;

&lt;p&gt;You don’t need to predict every requirement your application will ever have.&lt;/p&gt;

&lt;p&gt;You need a repeatable way to make the decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here’s a practical framework.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Identify the data and its relationships.
&lt;/h4&gt;

&lt;p&gt;List the main entities, how they relate, and which invariants must always hold. Do customers own orders? Can an order exist without its items? Must every payment reference a valid order?&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: List the important access patterns.
&lt;/h4&gt;

&lt;p&gt;Write down the queries the application will execute most frequently. Include filtering, sorting, joins, aggregation, search, and write operations.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Define consistency requirements.
&lt;/h4&gt;

&lt;p&gt;Determine which operations must be atomic, which data can be eventually consistent, and what happens if a read returns a slightly stale result.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 4: Estimate the workload.
&lt;/h4&gt;

&lt;p&gt;Think about expected read/write volume, concurrency, data growth, latency targets, and traffic patterns. Estimates do not need to be perfect, but assumptions should be explicit.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 5: Evaluate operational constraints.
&lt;/h4&gt;

&lt;p&gt;Consider hosting, backups, recovery objectives, monitoring, migrations, connection management, budget, and your team’s experience.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 6: Test the risky parts.
&lt;/h4&gt;

&lt;p&gt;Build a small proof of concept around the most demanding queries and transactions. Use realistic data volumes, inspect execution plans, and test concurrent operations.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 7: Record the trade-off.
&lt;/h4&gt;

&lt;p&gt;Document why you chose the database, what assumptions the choice depends on, and what evidence would justify revisiting it.&lt;/p&gt;

&lt;p&gt;A decision is much easier to evaluate when the reasoning behind it is visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A practical starting point&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ivakmy8wuwtr4m7mgv1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ivakmy8wuwtr4m7mgv1.png" alt=" " width="766" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are starting points, not universal prescriptions. Workload, consistency requirements, managed-service capabilities, and operational constraints can change the answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. The Best Database Decision Is the One You Can Explain
&lt;/h3&gt;

&lt;p&gt;A good engineer does not choose PostgreSQL because PostgreSQL is popular.&lt;/p&gt;

&lt;p&gt;They choose it because its relational model, transactional capabilities, indexing, query flexibility, ecosystem, and operational characteristics fit the system’s requirements.&lt;/p&gt;

&lt;p&gt;They don’t choose MongoDB merely because the schema changes frequently. They ask whether the data is naturally document-oriented, whether the access patterns fit the model, and how relationships and consistency should work.&lt;/p&gt;

&lt;p&gt;They don’t introduce Redis simply because a request is slow. They first determine whether the database is actually the bottleneck and whether caching is appropriate for the freshness requirements.&lt;/p&gt;

&lt;p&gt;The difference is not the technology.&lt;/p&gt;

&lt;p&gt;It’s the reasoning behind the choice.&lt;/p&gt;

&lt;p&gt;A database decision should be based on what the system needs to guarantee, how it needs to access its data, what workload it must support, and what complexity the team is prepared to own.&lt;/p&gt;

&lt;p&gt;You may eventually replace the database. Requirements change, workloads grow, and new constraints emerge.&lt;/p&gt;

&lt;p&gt;But you should not have to replace it because the original decision was based entirely on convenience or hype.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. When to Revisit Your Database Decision
&lt;/h3&gt;

&lt;p&gt;You don’t need to pick the “perfect” database on day one. But you do need to know when your current choice is becoming an architectural bottleneck.&lt;/p&gt;

&lt;p&gt;Signals it’s time to rethink:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You’re adding more caches than features.&lt;/li&gt;
&lt;li&gt;Your migrations regularly take hours or require maintenance windows.&lt;/li&gt;
&lt;li&gt;You can’t explain your consistency model in one sentence.&lt;/li&gt;
&lt;li&gt;Your team is afraid to touch certain tables.&lt;/li&gt;
&lt;li&gt;You’re building custom sharding logic in your application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, you’re not “tweaking” your database. You’re redesigning your architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;It’s easy to choose a database when all you’re thinking about is getting your application to work.&lt;/p&gt;

&lt;p&gt;The harder question is whether that choice will still make sense when your application has more data, more users, more complex business rules, and less room for failure.&lt;/p&gt;

&lt;p&gt;That’s when the real consequences begin to surface.&lt;/p&gt;

&lt;p&gt;The schema that once felt simple starts resisting new requirements. Queries that worked well at first become expensive. Concurrent requests expose inconsistencies you never anticipated. And the database you chose in a few minutes starts influencing decisions that take weeks to change.&lt;/p&gt;

&lt;p&gt;None of this means you need to predict every future problem or choose the most sophisticated database available. You don’t.&lt;/p&gt;

&lt;p&gt;What you need is a clear understanding of your application’s data, access patterns, consistency requirements, and operational constraints.&lt;/p&gt;

&lt;p&gt;Sometimes, that understanding will lead you to PostgreSQL. Sometimes, a document database or a specialized storage system will make more sense. And sometimes, the best decision is to stick with what you already have and fix the actual problem.&lt;/p&gt;

&lt;p&gt;The difference is that you’re no longer choosing based on hype, familiarity, or what everyone else seems to be using. You’re choosing based on what your system needs.&lt;/p&gt;

&lt;p&gt;Good backend engineering isn’t about choosing the most powerful technology. It’s about making decisions whose trade-offs you understand and can justify.&lt;/p&gt;

&lt;p&gt;Your database is one of those decisions.&lt;/p&gt;

&lt;p&gt;Choose it deliberately.&lt;/p&gt;

</description>
      <category>backendengineering</category>
      <category>databasedesign</category>
      <category>architecture</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Coupling vs. Cohesion: The Two Forces That Shape Good Software</title>
      <dc:creator>Nsikan Patrick Adaowo</dc:creator>
      <pubDate>Thu, 17 Sep 2026 04:31:45 +0000</pubDate>
      <link>https://dev.to/nsikanadaowo/coupling-vs-cohesion-the-two-forces-that-shape-good-software-36gg</link>
      <guid>https://dev.to/nsikanadaowo/coupling-vs-cohesion-the-two-forces-that-shape-good-software-36gg</guid>
      <description>&lt;p&gt;Good software is not only about making each component work.&lt;/p&gt;

&lt;p&gt;It is also about deciding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What belongs together?&lt;/li&gt;
&lt;li&gt;What should stay separate?&lt;/li&gt;
&lt;li&gt;Which modules should know about each other?&lt;/li&gt;
&lt;li&gt;How much knowledge should one component have about another?&lt;/li&gt;
&lt;li&gt;What should happen when one part of the system changes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two concepts help answer these questions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cohesion: how closely related the responsibilities inside a module are.&lt;/p&gt;

&lt;p&gt;Coupling: how dependent one module is on other modules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The goal is usually simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;High cohesion within modules and low coupling between modules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This sounds like a theoretical software engineering principle, but it directly affects how easy your system is to understand, test, scale, and change.&lt;/p&gt;

&lt;h2&gt;
  
  
  It works, but is that enough?
&lt;/h2&gt;

&lt;p&gt;When developers build a feature under pressure, the first goal is usually to make it work.&lt;/p&gt;

&lt;p&gt;That is understandable.&lt;/p&gt;

&lt;p&gt;A client needs the feature. A product manager wants the release. A deadline is approaching. You write the endpoint, connect the database, call the external provider, return the response, and move on.&lt;/p&gt;

&lt;p&gt;At that moment, the code may look perfectly fine.&lt;/p&gt;

&lt;p&gt;The trouble starts when the system grows.&lt;/p&gt;

&lt;p&gt;A simple order service begins to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate customer information.&lt;/li&gt;
&lt;li&gt;Calculate prices.&lt;/li&gt;
&lt;li&gt;Apply discounts.&lt;/li&gt;
&lt;li&gt;Check inventory.&lt;/li&gt;
&lt;li&gt;Process payment.&lt;/li&gt;
&lt;li&gt;Send emails.&lt;/li&gt;
&lt;li&gt;Publish events.&lt;/li&gt;
&lt;li&gt;Write audit logs.&lt;/li&gt;
&lt;li&gt;Generate invoices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small change to the payment provider now requires opening the order service. A change to email behaviour affects order creation. A new discount rule breaks an unrelated checkout flow.&lt;br&gt;
The code still works—but it has become expensive to change.&lt;/p&gt;

&lt;p&gt;This is often a design problem involving coupling and cohesion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is cohesion?
&lt;/h2&gt;

&lt;p&gt;Cohesion describes how closely related the responsibilities inside a module are.&lt;/p&gt;

&lt;p&gt;A highly cohesive module focuses on one clear purpose.&lt;/p&gt;

&lt;p&gt;For example, an authentication module may contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login.&lt;/li&gt;
&lt;li&gt;Logout.&lt;/li&gt;
&lt;li&gt;Token refresh.&lt;/li&gt;
&lt;li&gt;Password reset.&lt;/li&gt;
&lt;li&gt;Session validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These responsibilities are related because they all belong to identity and access management.&lt;/p&gt;

&lt;p&gt;A module that contains authentication, invoice generation, image resizing, and product recommendations has low cohesion.&lt;/p&gt;

&lt;p&gt;The code may still compile, but the module does not have a clear identity.&lt;/p&gt;

&lt;p&gt;A simple analogy&lt;br&gt;
Imagine a toolbox.&lt;/p&gt;

&lt;p&gt;A toolbox containing screwdrivers, pliers, measuring tape, and a wrench has a clear purpose: helping you perform repairs.&lt;/p&gt;

&lt;p&gt;Now imagine opening the same toolbox and finding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cooking utensils.&lt;/li&gt;
&lt;li&gt;Medical supplies.&lt;/li&gt;
&lt;li&gt;Office stationery.&lt;/li&gt;
&lt;li&gt;Car keys.&lt;/li&gt;
&lt;li&gt;Gardening tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The box may contain useful things, but it is no longer organized around one meaningful purpose.&lt;/p&gt;

&lt;p&gt;Low-cohesion software feels the same way.&lt;/p&gt;

&lt;p&gt;You find unrelated functionality in the same class or module, and you are never sure where the next responsibility should go.&lt;/p&gt;

&lt;p&gt;High cohesion in code&lt;br&gt;
Consider this service:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjtekyvmd48c6xbj2ooym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjtekyvmd48c6xbj2ooym.png" alt=" " width="672" height="708"&gt;&lt;/a&gt;&lt;br&gt;
The name UserService does not explain the actual boundaries.&lt;/p&gt;

&lt;p&gt;Some methods deal with users. Others deal with communication, billing, and reporting.&lt;/p&gt;

&lt;p&gt;A more cohesive design might separate them:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2jwq7vthmpld88boa8lp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2jwq7vthmpld88boa8lp.png" alt=" " width="772" height="1154"&gt;&lt;/a&gt;&lt;br&gt;
Now each module has a clearer purpose.&lt;/p&gt;

&lt;p&gt;When a developer needs to change password-reset behavior, they know where to look.&lt;/p&gt;

&lt;p&gt;When invoice requirements change, authentication code is less likely to be affected.&lt;/p&gt;

&lt;p&gt;That is the practical benefit of cohesion: related things stay together, and unrelated things stay apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is coupling?
&lt;/h2&gt;

&lt;p&gt;Coupling describes the degree of dependency between different modules.&lt;/p&gt;

&lt;p&gt;Two modules are tightly coupled when one knows too much about the other or depends heavily on its internal details.&lt;/p&gt;

&lt;p&gt;A change in one module may force changes in several others.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fly6gtuyujygs0216ohit.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fly6gtuyujygs0216ohit.png" alt=" " width="800" height="615"&gt;&lt;/a&gt;&lt;br&gt;
This service is coupled to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paystack.&lt;/li&gt;
&lt;li&gt;Prisma.&lt;/li&gt;
&lt;li&gt;A specific payment response format.&lt;/li&gt;
&lt;li&gt;A specific database implementation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you replace Paystack, change the database library, or alter the payment workflow, this service must change.&lt;/p&gt;

&lt;p&gt;That is tight coupling.&lt;/p&gt;

&lt;p&gt;A loosely coupled design depends on stable contracts instead:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1payf0pkwtw95g84g9p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1payf0pkwtw95g84g9p.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The order service depends on capabilities, not concrete infrastructure:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41l8vq4ey3plunhsgv6f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41l8vq4ey3plunhsgv6f.png" alt=" " width="800" height="751"&gt;&lt;/a&gt;&lt;br&gt;
Now the payment provider and database implementation can change behind the abstractions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cohesion and coupling work together
&lt;/h2&gt;

&lt;p&gt;Cohesion and coupling are not competing goals.&lt;/p&gt;

&lt;p&gt;They describe two sides of software structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cohesion looks inside a module.&lt;/li&gt;
&lt;li&gt;Coupling looks between modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A healthy design aims for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High cohesion inside&lt;/li&gt;
&lt;li&gt;Low coupling between&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of a well-organized company.&lt;/p&gt;

&lt;p&gt;Each department focuses on a specific responsibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finance handles finances.&lt;/li&gt;
&lt;li&gt;Human resources handles people operations.&lt;/li&gt;
&lt;li&gt;Sales handles customers and revenue.&lt;/li&gt;
&lt;li&gt;Engineering handles product development.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The departments still communicate, but they do so through clear responsibilities and defined processes.&lt;/p&gt;

&lt;p&gt;If every department performs every task, the organization becomes chaotic.&lt;/p&gt;

&lt;p&gt;If departments cannot communicate at all, work also stops.&lt;/p&gt;

&lt;p&gt;Software modules behave similarly.&lt;/p&gt;

&lt;p&gt;The goal is not zero coupling.&lt;/p&gt;

&lt;p&gt;The goal is controlled coupling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why zero coupling is impossible
&lt;/h2&gt;

&lt;p&gt;Every useful system contains dependencies.&lt;/p&gt;

&lt;p&gt;An order system needs payment information. A dashboard needs reporting data. A notification service needs information about events. A controller needs an application service.&lt;/p&gt;

&lt;p&gt;The goal is not to remove every dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The goal is to make dependencies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Necessary.&lt;/li&gt;
&lt;li&gt;Explicit.&lt;/li&gt;
&lt;li&gt;Stable.&lt;/li&gt;
&lt;li&gt;Small.&lt;/li&gt;
&lt;li&gt;Easy to replace.&lt;/li&gt;
&lt;li&gt;Easy to test.&lt;/li&gt;
&lt;li&gt;Easy to understand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A controller depending on an application service is normal.&lt;/p&gt;

&lt;p&gt;An application service knowing the private implementation details of three databases, two payment providers, and an email server is a warning sign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of coupling
&lt;/h2&gt;

&lt;p&gt;Coupling can appear in different forms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content coupling&lt;/strong&gt;&lt;br&gt;
One module directly accesses or modifies the internal data of another module.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqkgrxszxlu1gl9zj80u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqkgrxszxlu1gl9zj80u.png" alt=" " width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is highly dangerous because the consuming module depends on implementation details.&lt;/p&gt;

&lt;p&gt;If internalState changes, the order service breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common coupling&lt;/strong&gt;&lt;br&gt;
Multiple modules depend on shared global state.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvy06p1mx4tezkuximec8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvy06p1mx4tezkuximec8.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;br&gt;
Global state can make behavior difficult to predict and test.&lt;/p&gt;

&lt;p&gt;One module may modify the state while another module assumes it has not changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control coupling&lt;/strong&gt;&lt;br&gt;
One module tells another how to behave by passing flags.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F64nov1l2dilblmwkx3us.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F64nov1l2dilblmwkx3us.png" alt=" " width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This can become problematic when the receiving service accumulates many branches:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1rgyo7s27ryv6on1koue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1rgyo7s27ryv6on1koue.png" alt=" " width="799" height="458"&gt;&lt;/a&gt;&lt;br&gt;
Sometimes a strategy or channel abstraction is clearer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data coupling&lt;/strong&gt;&lt;br&gt;
One module passes only the data another module needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvlwww5dg7w8vkbcj28m8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvlwww5dg7w8vkbcj28m8.png" alt=" " width="688" height="522"&gt;&lt;/a&gt;&lt;br&gt;
This is generally healthier because the dependency is explicit and limited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message coupling&lt;/strong&gt;&lt;br&gt;
Modules communicate through messages or events without depending heavily on each other’s internal structure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0pvoggj8bcbs4zxpq2a3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0pvoggj8bcbs4zxpq2a3.png" alt=" " width="672" height="522"&gt;&lt;/a&gt;&lt;br&gt;
Message-based communication can reduce direct coupling, though it introduces other concerns such as event versioning, delivery guarantees, retries, and eventual consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of cohesion
&lt;/h2&gt;

&lt;p&gt;Cohesion can also vary in strength.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Functional cohesion&lt;/strong&gt;&lt;br&gt;
A module contains elements that work together to perform one well-defined task.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8j4f82k74ecvhd8vv82t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8j4f82k74ecvhd8vv82t.png" alt=" " width="800" height="401"&gt;&lt;/a&gt;&lt;br&gt;
Both methods belong to password hashing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequential cohesion&lt;/strong&gt;&lt;br&gt;
One operation produces data consumed by another operation within the same module.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fafbrzj5fygfjesz697fv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fafbrzj5fygfjesz697fv.png" alt=" " width="688" height="558"&gt;&lt;/a&gt;&lt;br&gt;
These operations form a clear sequence in a file-import workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Communicational cohesion&lt;/strong&gt;&lt;br&gt;
Several operations work on the same data.&lt;/p&gt;

&lt;p&gt;For example, a profile module may validate, update, and format user-profile data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coincidental cohesion&lt;/strong&gt;&lt;br&gt;
A module contains unrelated utilities simply because they were created around the same time.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjx4n9iror1hhviark3ei.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjx4n9iror1hhviark3ei.png" alt=" " width="704" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This kind of module often grows into a dumping ground.&lt;/p&gt;

&lt;p&gt;The problem with utility folders is not that utilities are always bad. The problem is that unrelated behaviour becomes difficult to own, test, and discover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A practical example: order processing&lt;/strong&gt;&lt;br&gt;
Let us compare two designs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low cohesion and high coupling&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsnsj1tusete2j20akip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsnsj1tusete2j20akip.png" alt=" " width="800" height="657"&gt;&lt;/a&gt;&lt;br&gt;
This service has many responsibilities.&lt;/p&gt;

&lt;p&gt;It is also coupled to several infrastructure systems.&lt;/p&gt;

&lt;p&gt;Possible consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large and slow tests.&lt;/li&gt;
&lt;li&gt;Difficult mocking.&lt;/li&gt;
&lt;li&gt;High risk of regression.&lt;/li&gt;
&lt;li&gt;Unclear error handling.&lt;/li&gt;
&lt;li&gt;Difficult reuse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every change affects the same class.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High cohesion and controlled coupling&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnqg9lb8u8mmht0nulfc7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnqg9lb8u8mmht0nulfc7.png" alt=" " width="551" height="203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each component owns a focused responsibility.&lt;/p&gt;

&lt;p&gt;The use case still coordinates the workflow, but the details are distributed across meaningful boundaries.&lt;/p&gt;

&lt;p&gt;This design is not automatically perfect.&lt;/p&gt;

&lt;p&gt;It may introduce more files and abstractions.&lt;/p&gt;

&lt;p&gt;But if the application is complex and these concerns change independently, the separation creates value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coupling in a NestJS application
&lt;/h2&gt;

&lt;p&gt;NestJS encourages dependency injection, which can help reduce coupling.&lt;/p&gt;

&lt;p&gt;A service can receive dependencies through its constructor:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69wnsza3zn19kbdll89y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69wnsza3zn19kbdll89y.png" alt=" " width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is better than constructing concrete dependencies inside the service:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39keyq73xd683pjkzd10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39keyq73xd683pjkzd10.png" alt=" " width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Constructor injection makes dependencies visible.&lt;/p&gt;

&lt;p&gt;It also makes testing easier:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj5mt5paynp8n8bko4902.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj5mt5paynp8n8bko4902.png" alt=" " width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The service can be tested without connecting to a real database or sending real emails.&lt;/p&gt;

&lt;p&gt;However, dependency injection alone does not guarantee low coupling.&lt;/p&gt;

&lt;p&gt;You can still inject a massive concrete service that exposes too many responsibilities.&lt;/p&gt;

&lt;p&gt;The quality of the boundary matters more than the presence of the injection mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coupling in modular monoliths
&lt;/h2&gt;

&lt;p&gt;A modular monolith may run as one deployable application while maintaining strong internal boundaries.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzcrsetr0ahl7numlxiie.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzcrsetr0ahl7numlxiie.png" alt=" " width="533" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The modules can communicate through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public application interfaces.&lt;/li&gt;
&lt;li&gt;Domain events.&lt;/li&gt;
&lt;li&gt;Commands.&lt;/li&gt;
&lt;li&gt;Queries.&lt;/li&gt;
&lt;li&gt;Shared contracts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They should avoid reaching directly into one another’s internal repositories or private tables without a clear reason.&lt;/p&gt;

&lt;p&gt;A modular monolith can provide many benefits of good boundaries without immediately introducing the operational complexity of microservices.&lt;/p&gt;

&lt;p&gt;This is especially useful for early-stage SaaS products.&lt;/p&gt;

&lt;p&gt;You can keep deployment simple while keeping responsibilities organized.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coupling in microservices
&lt;/h2&gt;

&lt;p&gt;Microservices do not automatically create low coupling.&lt;/p&gt;

&lt;p&gt;A system can have separate deployable services and still be tightly coupled if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every request requires five synchronous service calls.&lt;/li&gt;
&lt;li&gt;Services share the same database tables.&lt;/li&gt;
&lt;li&gt;One service depends on another’s internal schema.&lt;/li&gt;
&lt;li&gt;All services must deploy together.&lt;/li&gt;
&lt;li&gt;A small contract change breaks many consumers.&lt;/li&gt;
&lt;li&gt;A single service failure brings down the entire workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is distributed coupling.&lt;/p&gt;

&lt;p&gt;The services are separate in code and deployment,but tightly connected in behaviour.&lt;/p&gt;

&lt;p&gt;A well-designed microservice should own a meaningful capability and communicate through stable contracts.&lt;/p&gt;

&lt;p&gt;But even then, network communication introduces costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latency.&lt;/li&gt;
&lt;li&gt;Timeouts.&lt;/li&gt;
&lt;li&gt;Retries.&lt;/li&gt;
&lt;li&gt;Partial failures.&lt;/li&gt;
&lt;li&gt;Versioning.&lt;/li&gt;
&lt;li&gt;Observability.&lt;/li&gt;
&lt;li&gt;Eventual consistency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes a well-modularized monolith has less harmful coupling than a poorly designed microservice architecture.&lt;/p&gt;

&lt;p&gt;The number of services is not a measure of design quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cohesion and database design
&lt;/h2&gt;

&lt;p&gt;Coupling and cohesion also apply to data.&lt;/p&gt;

&lt;p&gt;A module should ideally own the data required for its responsibility.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Billing module → subscriptions, invoices, billing transactions&lt;/li&gt;
&lt;li&gt;Inventory module → stock levels, reservations, warehouses&lt;/li&gt;
&lt;li&gt;Identity module → users, credentials, sessions, permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If every module directly reads and writes every table, the database becomes a hidden integration layer.&lt;/p&gt;

&lt;p&gt;Changes become risky because nobody knows which services depend on which columns, indexes, or status values.&lt;/p&gt;

&lt;p&gt;This does not mean every module must have a separate database.&lt;/p&gt;

&lt;p&gt;A modular monolith can use one database while still enforcing ownership boundaries.&lt;/p&gt;

&lt;p&gt;The important question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Who owns this data, and how should other modules access it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Clear ownership improves cohesion.&lt;br&gt;
Controlled access reduces coupling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coupling and event-driven design
&lt;/h2&gt;

&lt;p&gt;Events can reduce direct dependency between modules.&lt;/p&gt;

&lt;p&gt;Instead of the order module directly calling the notification module:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv2vqsw19hq4463gnyu7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv2vqsw19hq4463gnyu7n.png" alt=" " width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;it may publish an event:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe42obf9zsaqdj75ib4t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe42obf9zsaqdj75ib4t.png" alt=" " width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The notification module subscribes to that event.&lt;/p&gt;

&lt;p&gt;This creates looser direct coupling because the order module does not need to know how notifications work.&lt;/p&gt;

&lt;p&gt;However, event-driven systems introduce new trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Events may be delayed.&lt;/li&gt;
&lt;li&gt;Events may be delivered more than once.&lt;/li&gt;
&lt;li&gt;Consumers may process events out of order.&lt;/li&gt;
&lt;li&gt;Event schemas must evolve carefully.&lt;/li&gt;
&lt;li&gt;Debugging requires tracing across asynchronous flows.&lt;/li&gt;
&lt;li&gt;Data may become eventually consistent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Events reduce some forms of coupling, but they do not eliminate system complexity.&lt;/p&gt;

&lt;p&gt;They move the complexity from direct calls to contracts, delivery, and observability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signs of high coupling
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your codebase may be tightly coupled when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small change requires edits across many modules.&lt;/li&gt;
&lt;li&gt;Tests need the entire application to run.&lt;/li&gt;
&lt;li&gt;Classes instantiate their own dependencies.&lt;/li&gt;
&lt;li&gt;Modules access one another’s private data.&lt;/li&gt;
&lt;li&gt;A shared utility module contains business logic for everything.&lt;/li&gt;
&lt;li&gt;Multiple services depend on the same database schema.&lt;/li&gt;
&lt;li&gt;Replacing a provider requires changing business logic.&lt;/li&gt;
&lt;li&gt;One failed dependency causes unrelated features to fail.&lt;/li&gt;
&lt;li&gt;Developers avoid refactoring certain areas because the impact is unpredictable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;High coupling creates a change ripple.&lt;/p&gt;

&lt;p&gt;One adjustment travels through the system, touching code that should not have been affected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signs of low cohesion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A module may have low cohesion when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its name is vague, such as CommonService or Utils.&lt;/li&gt;
&lt;li&gt;Its methods serve unrelated business functions.&lt;/li&gt;
&lt;li&gt;It changes frequently for different reasons.&lt;/li&gt;
&lt;li&gt;Developers are unsure where to add new functionality.&lt;/li&gt;
&lt;li&gt;The module has too many dependencies.&lt;/li&gt;
&lt;li&gt;Its tests cover unrelated behaviour.&lt;/li&gt;
&lt;li&gt;Removing one method leaves the remaining methods unrelated.&lt;/li&gt;
&lt;li&gt;The module requires a long explanation before anyone understands its purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If I had to describe this module in one sentence, would the description be clear?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is no, the module may need a better boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to improve cohesion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Group behavior by business capability&lt;/strong&gt;&lt;br&gt;
Instead of organizing everything by technical type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;controllers/&lt;/li&gt;
&lt;li&gt;services/&lt;/li&gt;
&lt;li&gt;repositories/&lt;/li&gt;
&lt;li&gt;utils/&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;consider also thinking in terms of capabilities:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyrpmrsgqwki6w13p0x3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyrpmrsgqwki6w13p0x3j.png" alt=" " width="577" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The best structure depends on the application, but grouping related behavior makes ownership clearer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep related data and behavior close&lt;/strong&gt;&lt;br&gt;
If a business rule always operates on a particular concept, consider keeping the rule near that concept.&lt;/p&gt;

&lt;p&gt;For example, order status transitions may belong to an order domain model or order service rather than a general utility file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate unrelated reasons to change&lt;/strong&gt;&lt;br&gt;
If one module changes because of payment requirements, email requirements, and reporting requirements, it probably contains multiple responsibilities.&lt;/p&gt;

&lt;p&gt;Split it around those change patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid generic dumping grounds&lt;/strong&gt;&lt;br&gt;
Be cautious with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;utils.&lt;/li&gt;
&lt;li&gt;helpers.&lt;/li&gt;
&lt;li&gt;common.&lt;/li&gt;
&lt;li&gt;misc.&lt;/li&gt;
&lt;li&gt;shared-service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These folders can be useful, but they often become places where code is stored without a clear owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to reduce coupling
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Depend on abstractions&lt;/strong&gt;&lt;br&gt;
Define contracts around capabilities:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk6cy2mufuvfc7rogkjoy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk6cy2mufuvfc7rogkjoy.png" alt=" " width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application service should not need to know the database library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use dependency injection&lt;/strong&gt;&lt;br&gt;
Pass dependencies into a component instead of constructing them internally.&lt;/p&gt;

&lt;p&gt;This makes dependencies explicit and replaceable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encapsulate internal details&lt;/strong&gt;&lt;br&gt;
Expose what another module needs, not everything the module knows.&lt;/p&gt;

&lt;p&gt;A module should not expose its internal database models, private state, and implementation-specific helper methods unnecessarily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer messages for some workflows&lt;/strong&gt;&lt;br&gt;
Events and commands can reduce direct knowledge between modules.&lt;/p&gt;

&lt;p&gt;Use them where asynchronous behavior and eventual consistency are acceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Define stable contracts&lt;/strong&gt;&lt;br&gt;
Use clear request and response models.&lt;/p&gt;

&lt;p&gt;Avoid passing internal database entities everywhere. Internal data structures change more frequently than business contracts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limit shared mutable state&lt;/strong&gt;&lt;br&gt;
Shared mutable state is one of the easiest ways to create unpredictable coupling.&lt;/p&gt;

&lt;p&gt;Prefer explicit inputs and outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The balance: cohesion versus coupling
&lt;/h2&gt;

&lt;p&gt;It is possible to overcorrect.&lt;/p&gt;

&lt;p&gt;Suppose you split one simple function into ten classes.&lt;/p&gt;

&lt;p&gt;You may reduce local responsibilities, but create excessive coordination and indirection.&lt;/p&gt;

&lt;p&gt;This is sometimes called accidental complexity.&lt;/p&gt;

&lt;p&gt;A good architecture balances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cohesion.&lt;/li&gt;
&lt;li&gt;Coupling.&lt;/li&gt;
&lt;li&gt;Simplicity.&lt;/li&gt;
&lt;li&gt;Performance.&lt;/li&gt;
&lt;li&gt;Team understanding.&lt;/li&gt;
&lt;li&gt;Operational cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not want modules so large that everything is mixed together.&lt;/p&gt;

&lt;p&gt;You also do not want modules so fragmented that understanding one workflow requires jumping through twenty files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The right boundary is usually where:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Responsibilities change together.&lt;/li&gt;
&lt;li&gt;Data is accessed together.&lt;/li&gt;
&lt;li&gt;Business rules belong together.&lt;/li&gt;
&lt;li&gt;The module can be explained clearly.&lt;/li&gt;
&lt;li&gt;The dependency is meaningful.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A useful code review framework
&lt;/h2&gt;

&lt;p&gt;When reviewing a module, ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About cohesion&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do these methods belong to the same business capability?&lt;/li&gt;
&lt;li&gt;Do they use the same data?&lt;/li&gt;
&lt;li&gt;Do they change for similar reasons?&lt;/li&gt;
&lt;li&gt;Does the module have one clear purpose?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;About coupling&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What does this module know about other modules?&lt;/li&gt;
&lt;li&gt;Does it depend on implementation details?&lt;/li&gt;
&lt;li&gt;Can a dependency be replaced easily?&lt;/li&gt;
&lt;li&gt;Does a small change here affect many consumers?&lt;/li&gt;
&lt;li&gt;Are dependencies explicit?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;About boundaries&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who owns this business rule?&lt;/li&gt;
&lt;li&gt;Who owns this data?&lt;/li&gt;
&lt;li&gt;Is this communication synchronous or asynchronous?&lt;/li&gt;
&lt;li&gt;Is the contract stable?&lt;/li&gt;
&lt;li&gt;Is the abstraction solving a real problem?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions help you evaluate design beyond whether the code is syntactically clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The connection to SOLID
&lt;/h2&gt;

&lt;p&gt;Coupling and cohesion are closely connected to SOLID principles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single Responsibility improves cohesion.&lt;/li&gt;
&lt;li&gt;Open/Closed reduces unnecessary changes to stable modules.&lt;/li&gt;
&lt;li&gt;Liskov Substitution creates reliable contracts.&lt;/li&gt;
&lt;li&gt;Interface Segregation reduces unnecessary dependencies.&lt;/li&gt;
&lt;li&gt;Dependency Inversion reduces coupling to infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are also related to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Encapsulation.&lt;/li&gt;
&lt;li&gt;Separation of concerns.&lt;/li&gt;
&lt;li&gt;Domain-driven design.&lt;/li&gt;
&lt;li&gt;Modular architecture.&lt;/li&gt;
&lt;li&gt;Clean architecture.&lt;/li&gt;
&lt;li&gt;Hexagonal architecture.&lt;/li&gt;
&lt;li&gt;Event-driven design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the underlying idea remains simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Put related responsibilities together, and prevent unrelated components from knowing too much about one another.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Good software is not made of isolated components that never communicate.&lt;/p&gt;

&lt;p&gt;It is made of focused components that communicate intentionally.&lt;/p&gt;

&lt;p&gt;High cohesion gives each module a clear identity.&lt;/p&gt;

&lt;p&gt;Low coupling prevents changes from spreading unnecessarily.&lt;/p&gt;

&lt;p&gt;When cohesion is low, modules become confusing.&lt;/p&gt;

&lt;p&gt;When coupling is high, the entire system becomes fragile.&lt;/p&gt;

&lt;p&gt;When both are poor, every feature becomes a negotiation with the existing codebase.&lt;/p&gt;

&lt;p&gt;The goal is not to eliminate dependencies.&lt;/p&gt;

&lt;p&gt;The goal is to make dependencies deliberate, visible, and manageable.&lt;/p&gt;

&lt;p&gt;So, the next time you create a service, module, class, or microservice, ask two questions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does everything inside this component belong together?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this component know more about the outside world than it needs to know?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those two questions can prevent a lot of future pain.&lt;/p&gt;

&lt;p&gt;Because maintainable software is not software with no complexity.&lt;/p&gt;

&lt;p&gt;It is software where complexity has been given a clear place to live.&lt;/p&gt;

</description>
      <category>softwaredevelopment</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>When Abstraction Becomes a Bottleneck: The Real Cost of Overengineering</title>
      <dc:creator>Nsikan Patrick Adaowo</dc:creator>
      <pubDate>Thu, 10 Sep 2026 14:04:08 +0000</pubDate>
      <link>https://dev.to/nsikanadaowo/when-abstraction-becomes-a-bottleneck-the-real-cost-of-overengineering-c57</link>
      <guid>https://dev.to/nsikanadaowo/when-abstraction-becomes-a-bottleneck-the-real-cost-of-overengineering-c57</guid>
      <description>&lt;p&gt;Abstraction is one of the most powerful tools in software engineering.&lt;/p&gt;

&lt;p&gt;It hides complexity. It reveals intent. It reduces duplication. It makes systems easier to understand, test, and change.&lt;br&gt;
But abstraction has a shadow side.&lt;/p&gt;

&lt;p&gt;When applied too early, too generically, or without a clear problem, abstraction can become a bottleneck.&lt;/p&gt;

&lt;p&gt;It can make code harder to read, harder to debug, and harder to evolve.&lt;/p&gt;

&lt;p&gt;It can turn simple workflows into mazes of indirection.&lt;/p&gt;

&lt;p&gt;It can create the illusion of flexibility while introducing hidden coupling.&lt;/p&gt;

&lt;p&gt;And perhaps most dangerously, it can make engineers feel productive while actually reducing the team’s ability to ship.&lt;/p&gt;

&lt;p&gt;This article explores when abstraction becomes harmful, why overengineering is more expensive than duplication, and how to recognize the signs that your design is working against you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The promise of abstraction
&lt;/h2&gt;

&lt;p&gt;Abstraction is how we manage complexity.&lt;/p&gt;

&lt;p&gt;Instead of thinking in terms of bits and bytes, we work with concepts like users, orders, payments, and notifications.&lt;/p&gt;

&lt;p&gt;Instead of repeating the same logic in ten places, we extract a function.&lt;/p&gt;

&lt;p&gt;Instead of coupling business rules to a specific database, we define a repository interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good abstractions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduce cognitive load.&lt;/li&gt;
&lt;li&gt;Clarify intent.&lt;/li&gt;
&lt;li&gt;Make change safer.&lt;/li&gt;
&lt;li&gt;Enable reuse.&lt;/li&gt;
&lt;li&gt;Protect core logic from infrastructure details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are essential for building maintainable systems.&lt;/p&gt;

&lt;p&gt;But not every abstraction is good.&lt;/p&gt;

&lt;p&gt;And not every problem needs an abstraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden cost of premature abstraction
&lt;/h2&gt;

&lt;p&gt;Premature abstraction happens when you generalize a solution before you fully understand the problem.&lt;/p&gt;

&lt;p&gt;You see a pattern that appears twice. You assume it will appear a third time. You create an interface, a factory, a strategy, and a configuration object to support every possible future variation.&lt;/p&gt;

&lt;p&gt;The code looks sophisticated.&lt;/p&gt;

&lt;p&gt;But the abstraction is built on assumptions, not evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asymmetry of technical debt
&lt;/h2&gt;

&lt;p&gt;There is an important asymmetry between premature optimization and premature abstraction.&lt;/p&gt;

&lt;p&gt;Premature optimization usually affects a localized part of the code.&lt;/p&gt;

&lt;p&gt;For example, you implement a complex caching layer for a function that is called a few times per day. The optimization is unnecessary, but its impact is limited.&lt;/p&gt;

&lt;p&gt;Premature abstraction, on the other hand, can affect the entire architecture.&lt;/p&gt;

&lt;p&gt;When you introduce semantic boundaries and logical coupling before the problem domain is understood, you create a mesh of dependencies that resists evolution.&lt;/p&gt;

&lt;p&gt;The cost of unwinding a wrong abstraction is not linear.&lt;/p&gt;

&lt;p&gt;It is often geometric.&lt;/p&gt;

&lt;p&gt;You may need to dismantle entire subsystems, update multiple layers, and reconcile conflicting mental models.&lt;/p&gt;

&lt;p&gt;The cost of removing a premature optimization is typically isolated to a specific function or block.&lt;/p&gt;

&lt;p&gt;The cost of removing a premature abstraction can ripple through the entire codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signs that abstraction has become a bottleneck
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Too many layers of indirection&lt;/strong&gt;&lt;br&gt;
You need to jump through five files to understand a simple operation.&lt;/p&gt;

&lt;p&gt;Controller → Service → UseCase → Handler → Processor → Adapter&lt;/p&gt;

&lt;p&gt;Each layer adds indirection without adding clarity.&lt;/p&gt;

&lt;p&gt;The call chain is deep, but the business intent is unclear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Overly complex class hierarchies&lt;/strong&gt;&lt;br&gt;
Your class hierarchy has multiple levels of abstract classes and interfaces.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;BaseService&lt;/em&gt; → &lt;em&gt;AbstractCrudService&lt;/em&gt; → &lt;em&gt;EntityService&lt;/em&gt; → &lt;em&gt;UserService&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each layer introduces hooks, configuration options, and extension points that are rarely used.&lt;/p&gt;

&lt;p&gt;New developers cannot tell which methods are important and which are legacy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Methods with too many parameters&lt;/strong&gt;&lt;br&gt;
A method signature looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Folcmgc263memq1uy4ocj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Folcmgc263memq1uy4ocj.png" alt=" " width="790" height="856"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The method tries to support every possible scenario.&lt;/p&gt;

&lt;p&gt;As a result, it is difficult to call, difficult to test, and difficult to understand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Generic components that handle everything&lt;/strong&gt;&lt;br&gt;
A single component or service tries to support every use case.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4gc92h09wxtneafroaot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4gc92h09wxtneafroaot.png" alt=" " width="672" height="744"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The class becomes a dumping ground for unrelated functionality.&lt;/p&gt;

&lt;p&gt;Its name no longer communicates intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Configuration objects that hold everything&lt;/strong&gt;&lt;br&gt;
A configuration object accumulates settings for multiple concerns:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0s4nee85nk4u4tn6tmfr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0s4nee85nk4u4tn6tmfr.png" alt=" " width="790" height="930"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The configuration becomes unreadable and impossible to evolve safely.&lt;/p&gt;

&lt;p&gt;Changing one setting may affect unrelated parts of the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Names that hide intent&lt;/strong&gt;&lt;br&gt;
Classes and functions are named with generic terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manager&lt;/li&gt;
&lt;li&gt;Processor&lt;/li&gt;
&lt;li&gt;Handler&lt;/li&gt;
&lt;li&gt;Service&lt;/li&gt;
&lt;li&gt;Controller&lt;/li&gt;
&lt;li&gt;Provider&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The names do not convey what the component actually does.&lt;/p&gt;

&lt;p&gt;A developer must read the implementation to understand its purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Conditional branches that proliferate&lt;/strong&gt;&lt;br&gt;
A single function contains many conditional branches to support different scenarios:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff2r5c7j1px98uag79x7x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff2r5c7j1px98uag79x7x.png" alt=" " width="738" height="708"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The function tries to be versatile, but it becomes complex and fragile.&lt;/p&gt;

&lt;p&gt;Adding a new type requires modifying the central function and understanding all existing branches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Onboarding becomes extremely difficult&lt;/strong&gt;&lt;br&gt;
New developers take weeks to understand the codebase.&lt;/p&gt;

&lt;p&gt;They must decipher multiple layers of abstraction before they can make a simple change.&lt;/p&gt;

&lt;p&gt;The joy of building yields to the chore of deciphering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Breaking one abstraction breaks the whole&lt;/strong&gt;&lt;br&gt;
A single abstraction is used by many parts of the system.&lt;/p&gt;

&lt;p&gt;When you change it, multiple features break.&lt;/p&gt;

&lt;p&gt;The abstraction has become a concentration risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Domain intent disappears&lt;/strong&gt;&lt;br&gt;
The code no longer reflects the business domain.&lt;/p&gt;

&lt;p&gt;Instead of Order, Payment, and Invoice, you have Entity, Resource, and Model.&lt;/p&gt;

&lt;p&gt;Instead of &lt;em&gt;chargePayment&lt;/em&gt;, you have &lt;em&gt;executeAction&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The connection between the code and the problem it solves is lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cost of overengineering
&lt;/h2&gt;

&lt;p&gt;Overengineering is not just an aesthetic problem.&lt;/p&gt;

&lt;p&gt;It has real costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Slower development&lt;/strong&gt;&lt;br&gt;
Features take longer to implement because engineers must navigate multiple layers of abstraction.&lt;/p&gt;

&lt;p&gt;Simple changes require understanding complex call chains.&lt;/p&gt;

&lt;p&gt;Estimates slip because the cognitive surface is larger than it appears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- More bugs&lt;/strong&gt;&lt;br&gt;
Abstractions that try to support too many scenarios introduce edge cases.&lt;/p&gt;

&lt;p&gt;Conditional branches multiply.&lt;/p&gt;

&lt;p&gt;Configuration options interact in unexpected ways.&lt;/p&gt;

&lt;p&gt;The system becomes harder to reason about, and bugs become harder to locate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Higher maintenance cost&lt;/strong&gt;&lt;br&gt;
Maintaining an overengineered system requires more effort.&lt;/p&gt;

&lt;p&gt;Developers must understand not only what the code does, but why it was designed this way.&lt;/p&gt;

&lt;p&gt;Refactoring becomes risky because the impact of changes is difficult to predict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Reduced morale&lt;/strong&gt;&lt;br&gt;
Engineers join teams to build things.&lt;/p&gt;

&lt;p&gt;When the codebase feels like a maze, motivation declines.&lt;/p&gt;

&lt;p&gt;Talent departs when the joy of building yields to the chore of deciphering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Lost opportunities&lt;/strong&gt;&lt;br&gt;
Overengineered systems resist change.&lt;/p&gt;

&lt;p&gt;New ideas are avoided because they do not fit the existing abstraction.&lt;/p&gt;

&lt;p&gt;The team spends more time reconciling mental models than shipping value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplication is cheaper than the wrong abstraction
&lt;/h2&gt;

&lt;p&gt;One of the most important lessons in software design is this:&lt;/p&gt;

&lt;p&gt;Duplication is far cheaper than the wrong abstraction.&lt;/p&gt;

&lt;p&gt;When you duplicate code, you pay a localized cost.&lt;/p&gt;

&lt;p&gt;When you change one copy, you may need to change the other.&lt;/p&gt;

&lt;p&gt;But the impact is limited.&lt;/p&gt;

&lt;p&gt;When you create the wrong abstraction, you pay a systemic cost.&lt;/p&gt;

&lt;p&gt;The abstraction couples multiple parts of the system.&lt;/p&gt;

&lt;p&gt;Changing it requires understanding all consumers.&lt;/p&gt;

&lt;p&gt;The cost of unwinding it can be geometric.&lt;/p&gt;

&lt;p&gt;This does not mean duplication is always good.&lt;/p&gt;

&lt;p&gt;It means that premature generalization is often more expensive than waiting until the pattern is clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A useful rule is the “rule of three”:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Implement the logic the first time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Implement it the second time, even if it looks similar.&lt;br&gt;
When you see it a third time, consider an abstraction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This rule is not absolute, but it encourages evidence-based design.&lt;/p&gt;

&lt;p&gt;You abstract when you have seen the pattern, not when you predict it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When abstraction is appropriate
&lt;/h2&gt;

&lt;p&gt;Abstraction is not the enemy.&lt;/p&gt;

&lt;p&gt;Bad abstraction is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good abstraction is appropriate when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The pattern has appeared multiple times.&lt;/li&gt;
&lt;li&gt;The variation is real, not speculative.&lt;/li&gt;
&lt;li&gt;The abstraction clarifies intent.&lt;/li&gt;
&lt;li&gt;The boundary is meaningful.&lt;/li&gt;
&lt;li&gt;The cost of change is reduced.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The team can understand and maintain it.&lt;br&gt;
For example, a payment gateway abstraction is useful when:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You have multiple payment providers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The business may change providers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The core logic should not depend on a specific provider.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Testing is easier with a fake implementation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A repository abstraction is useful when:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The data access strategy may change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The domain logic should not depend on a specific database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Testing is easier with an in-memory implementation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if you have one payment provider and no plan to change, a direct implementation may be simpler.&lt;/p&gt;

&lt;p&gt;If you have one database and no need for multiple implementations, a direct repository may be clearer.&lt;/p&gt;

&lt;p&gt;The goal is not to avoid abstraction.&lt;/p&gt;

&lt;p&gt;The goal is to avoid abstraction that does not yet earn its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The YAGNI principle
&lt;/h2&gt;

&lt;p&gt;YAGNI stands for “You Aren’t Going to Need It.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The principle says:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not implement functionality until it is necessary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This does not mean you should ignore the future.&lt;/p&gt;

&lt;p&gt;It means you should not implement speculative features or abstractions based on assumptions.&lt;/p&gt;

&lt;p&gt;There is no reason to make code more complicated just so that down the road you may be able to make it more flexible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;YAGNI complements other principles such as:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DRY (Don’t Repeat Yourself).&lt;/li&gt;
&lt;li&gt;KISS (Keep It Simple, Stupid).&lt;/li&gt;
&lt;li&gt;SOLID.&lt;/li&gt;
&lt;li&gt;Separation of concerns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The balance is important.&lt;/p&gt;

&lt;p&gt;DRY encourages removing duplication.&lt;/p&gt;

&lt;p&gt;YAGNI encourages waiting until the duplication is real.&lt;/p&gt;

&lt;p&gt;Together, they suggest:&lt;/p&gt;

&lt;p&gt;Do not repeat yourself unnecessarily, but do not abstract prematurely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WET before DRY&lt;/strong&gt;&lt;br&gt;
Another useful perspective is “WET before DRY.”&lt;/p&gt;

&lt;p&gt;WET stands for “Write Everything Twice.”&lt;/p&gt;

&lt;p&gt;The idea is that duplicating code once or twice is not costly.&lt;/p&gt;

&lt;p&gt;It allows you to understand the pattern before abstracting it.&lt;/p&gt;

&lt;p&gt;Starting with DRY from the beginning increases the chances of bad generalizations.&lt;/p&gt;

&lt;p&gt;When you attempt to DRY up the code too early, you may:&lt;/p&gt;

&lt;p&gt;Optimize prematurely.&lt;/p&gt;

&lt;p&gt;Bundle operations with different contexts.&lt;/p&gt;

&lt;p&gt;Create abstractions that do not fit the problem.&lt;/p&gt;

&lt;p&gt;An alternative is to see WET and DRY as complementary.&lt;/p&gt;

&lt;p&gt;Start with a basic design.&lt;/p&gt;

&lt;p&gt;When you find duplicated code two or three times, consider an abstraction.&lt;/p&gt;

&lt;p&gt;After working on the problem for some time, reflect on how to abstract based on the challenges you faced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical guidelines for avoiding over-abstraction
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Wait for evidence&lt;/strong&gt;&lt;br&gt;
Do not abstract based on predictions.&lt;/p&gt;

&lt;p&gt;Wait until you have seen the pattern multiple times.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;p&gt;Has this variation appeared in production?&lt;/p&gt;

&lt;p&gt;Is this a real requirement or a hypothetical scenario?&lt;/p&gt;

&lt;p&gt;Will this abstraction simplify the next feature?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Favor explicit code&lt;/strong&gt;&lt;br&gt;
Explicit code is easier to understand than clever abstractions.&lt;/p&gt;

&lt;p&gt;If a function has three clear steps, write them explicitly.&lt;/p&gt;

&lt;p&gt;If a workflow has four distinct operations, name them clearly.&lt;/p&gt;

&lt;p&gt;Do not hide simple logic behind layers of indirection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Keep abstractions close to the domain&lt;/strong&gt;&lt;br&gt;
Abstractions should reflect the business domain, not technical convenience.&lt;/p&gt;

&lt;p&gt;Use names like Order, Payment, and Invoice instead of Entity, Resource, and Model.&lt;/p&gt;

&lt;p&gt;Use verbs like chargePayment and createOrder instead of executeAction and processResource.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Limit configuration&lt;/strong&gt;&lt;br&gt;
Avoid configuration objects that hold everything.&lt;/p&gt;

&lt;p&gt;Split configuration by concern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database configuration.&lt;/li&gt;
&lt;li&gt;Feature flags.&lt;/li&gt;
&lt;li&gt;External API keys.&lt;/li&gt;
&lt;li&gt;Tenant settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the configuration easier to understand and evolve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Reduce indirection&lt;/strong&gt;&lt;br&gt;
Ask whether each layer adds value.&lt;/p&gt;

&lt;p&gt;If a layer only delegates to another layer without adding behaviour, consider removing it.&lt;/p&gt;

&lt;p&gt;Deep call chains increase cognitive load without improving design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Test the abstraction&lt;/strong&gt;&lt;br&gt;
Before adopting an abstraction, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can a new developer understand it in ten minutes?&lt;/li&gt;
&lt;li&gt;Does it make the next feature easier to implement?&lt;/li&gt;
&lt;li&gt;Does it reduce the number of concepts a developer must hold in their head?&lt;/li&gt;
&lt;li&gt;Does it make testing easier or harder?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is no, the abstraction may not be worth the cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Prefer composition over inheritance&lt;/strong&gt;&lt;br&gt;
Inheritance can create deep hierarchies that are difficult to understand.&lt;/p&gt;

&lt;p&gt;Composition allows you to assemble behaviour from smaller components.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ovy0i51bnsy092ntb4j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ovy0i51bnsy092ntb4j.png" alt=" " width="799" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is often clearer than a deep inheritance tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Document the intent&lt;/strong&gt;&lt;br&gt;
If an abstraction is necessary, document its intent.&lt;/p&gt;

&lt;p&gt;Explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why the abstraction exists.&lt;/li&gt;
&lt;li&gt;What problem it solves.&lt;/li&gt;
&lt;li&gt;What scenarios it supports.&lt;/li&gt;
&lt;li&gt;What scenarios it does not support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps future developers understand the design decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A practical example: notification service&lt;/strong&gt;&lt;br&gt;
Consider a notification system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overengineered design&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuy4ucaxiv9a1p3ssdhz8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuy4ucaxiv9a1p3ssdhz8.png" alt=" " width="800" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The abstraction tries to support every possible scenario.&lt;/p&gt;

&lt;p&gt;The configuration object holds settings for multiple concerns.&lt;/p&gt;

&lt;p&gt;The method contains many conditional branches.&lt;/p&gt;

&lt;p&gt;Adding a new channel requires modifying the central method and understanding all existing logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simpler design&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F36nfxc0ublzdyxhmgi3h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F36nfxc0ublzdyxhmgi3h.png" alt=" " width="800" height="799"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each channel is a separate implementation.&lt;/p&gt;

&lt;p&gt;The service delegates to the appropriate channel.&lt;/p&gt;

&lt;p&gt;Adding a new channel requires creating a new implementation, not modifying existing logic.&lt;/p&gt;

&lt;p&gt;The design is simpler, clearer, and easier to extend.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to refactor
&lt;/h2&gt;

&lt;p&gt;Refactoring is appropriate when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The pattern has appeared multiple times.&lt;/li&gt;
&lt;li&gt;The duplication is causing maintenance problems.&lt;/li&gt;
&lt;li&gt;The abstraction will clarify intent.&lt;/li&gt;
&lt;li&gt;The team can understand and maintain it.&lt;/li&gt;
&lt;li&gt;The cost of change is reduced.&lt;/li&gt;
&lt;li&gt;Refactoring is premature when:&lt;/li&gt;
&lt;li&gt;The pattern has appeared once.&lt;/li&gt;
&lt;li&gt;The variation is speculative.&lt;/li&gt;
&lt;li&gt;The abstraction adds indirection without clarity.&lt;/li&gt;
&lt;li&gt;The team cannot explain the design in simple terms.&lt;/li&gt;
&lt;li&gt;The cost of change increases.
The goal is not to avoid refactoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to refactor based on evidence, not prediction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The balance: Goldilocks abstraction
&lt;/h2&gt;

&lt;p&gt;Abstraction should be “just right.”&lt;/p&gt;

&lt;p&gt;Not too little, not too much.&lt;/p&gt;

&lt;p&gt;A codebase with too little abstraction can be repetitive and difficult to maintain.&lt;/p&gt;

&lt;p&gt;A codebase with too much abstraction can be difficult to understand and evolve.&lt;/p&gt;

&lt;p&gt;The sweet spot is when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The code reflects the domain.&lt;/li&gt;
&lt;li&gt;The abstractions are meaningful.&lt;/li&gt;
&lt;li&gt;The call chains are shallow.&lt;/li&gt;
&lt;li&gt;The intent is clear.&lt;/li&gt;
&lt;li&gt;The team can onboard quickly.&lt;/li&gt;
&lt;li&gt;Change is easier, not harder.&lt;/li&gt;
&lt;li&gt;This balance is not static.&lt;/li&gt;
&lt;li&gt;It evolves as the system grows and the team learns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important skill is recognizing when an abstraction has crossed the line from helpful to harmful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Abstraction is a tool, not a goal.&lt;/p&gt;

&lt;p&gt;Good abstraction reduces complexity.&lt;/p&gt;

&lt;p&gt;Bad abstraction creates it.&lt;/p&gt;

&lt;p&gt;The real cost of overengineering is not measured in lines of code.&lt;/p&gt;

&lt;p&gt;It is measured in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slower development.&lt;/li&gt;
&lt;li&gt;More bugs.&lt;/li&gt;
&lt;li&gt;Higher maintenance cost.&lt;/li&gt;
&lt;li&gt;Reduced morale.&lt;/li&gt;
&lt;li&gt;Lost opportunities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Duplication is often cheaper than the wrong abstraction.&lt;/p&gt;

&lt;p&gt;Waiting for evidence is often wiser than predicting the future.&lt;/p&gt;

&lt;p&gt;Explicit code is often clearer than clever design.&lt;/p&gt;

&lt;p&gt;The next time you create an abstraction, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this simplify the next feature?&lt;/li&gt;
&lt;li&gt;Can a new developer understand this in ten minutes?&lt;/li&gt;
&lt;li&gt;Is this pattern real or speculative?&lt;/li&gt;
&lt;li&gt;Does this reduce or increase the cognitive load?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because good software is not software with the most abstractions.&lt;/p&gt;

&lt;p&gt;It is software that can be understood, changed, and extended without unnecessary friction.&lt;/p&gt;

&lt;p&gt;And sometimes, the best design is the one that resists the urge to generalize before it is ready.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Dependency Injection Explained: Why Your Classes Shouldn't Know How Everything Is Built</title>
      <dc:creator>Nsikan Patrick Adaowo</dc:creator>
      <pubDate>Sun, 30 Aug 2026 12:31:31 +0000</pubDate>
      <link>https://dev.to/nsikanadaowo/dependency-injection-explained-why-your-classes-shouldnt-know-how-everything-is-built-77o</link>
      <guid>https://dev.to/nsikanadaowo/dependency-injection-explained-why-your-classes-shouldnt-know-how-everything-is-built-77o</guid>
      <description>&lt;p&gt;Your codebase may not be failing because it lacks features. It may be failing because everything knows too much about everything else.&lt;/p&gt;

&lt;p&gt;Most backend developers learn dependency injection through a framework.&lt;/p&gt;

&lt;p&gt;In NestJS, you add decorators. In Spring, you annotate classes. In Angular, you declare providers. The framework then constructs your objects and passes their dependencies.&lt;/p&gt;

&lt;p&gt;Everything works fine.&lt;/p&gt;

&lt;p&gt;But many developers stop at the mechanics.&lt;/p&gt;

&lt;p&gt;They learn how to use dependency injection, but not why it matters.&lt;/p&gt;

&lt;p&gt;They learn the syntax, but not the design principle.&lt;/p&gt;

&lt;p&gt;They learn the pattern, but not the problem it solves.&lt;/p&gt;

&lt;p&gt;The result is code that uses dependency injection but still feels tightly coupled, difficult to test, and fragile when requirements change.&lt;/p&gt;

&lt;p&gt;This article explains what dependency injection really is, why your classes should not know how their dependencies are built, and how to use it in a way that improves maintainability, testability, and flexibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: classes that build their own&amp;nbsp;world
&lt;/h2&gt;

&lt;p&gt;Consider this service:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatb884hd0gu57ks7t7yr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatb884hd0gu57ks7t7yr.png" alt=" " width="800" height="716"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This code may work.&lt;/p&gt;

&lt;p&gt;But it has a hidden problem.&lt;/p&gt;

&lt;p&gt;The OrderService knows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which database library to use.&lt;/li&gt;
&lt;li&gt;Which payment provider to call.&lt;/li&gt;
&lt;li&gt;Which email client to instantiate.&lt;/li&gt;
&lt;li&gt;How to construct each dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates tight coupling.&lt;/p&gt;

&lt;p&gt;If you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace Paystack with Flutterwave.&lt;/li&gt;
&lt;li&gt;Switch from Prisma to TypeORM.&lt;/li&gt;
&lt;li&gt;Use a different email provider.&lt;/li&gt;
&lt;li&gt;Test the service without calling real APIs.&lt;/li&gt;
&lt;li&gt;Use a different configuration in staging versus production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;you must modify the service itself.&lt;br&gt;
The class is responsible not only for its business logic, but also for constructing its entire world.&lt;br&gt;
This is the opposite of what dependency injection tries to achieve.&lt;/p&gt;




&lt;h2&gt;
  
  
  What dependency injection actually&amp;nbsp;is
&lt;/h2&gt;

&lt;p&gt;Dependency injection is a design pattern where a class receives its dependencies from the outside instead of creating them internally.&lt;br&gt;
Instead of:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3crvog4mwvny8uo512t4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3crvog4mwvny8uo512t4.png" alt=" " width="799" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You write:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4o2ot6r5p89ecg50hppe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4o2ot6r5p89ecg50hppe.png" alt=" " width="799" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dependency is provided by an external assembler, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A framework container.&lt;/li&gt;
&lt;li&gt;A factory function.&lt;/li&gt;
&lt;li&gt;A manual composition root.&lt;/li&gt;
&lt;li&gt;A dependency injection library.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The class no longer decides how its dependencies are built.&lt;br&gt;
It only declares what it needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Inversion of Control: the bigger&amp;nbsp;idea
&lt;/h2&gt;

&lt;p&gt;Dependency injection is a specific way to implement a broader principle called Inversion of Control.&lt;br&gt;
In traditional code, a high-level component directly creates or locates its low-level dependencies.&lt;br&gt;
For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tmplou4cxne6o1geyqa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tmplou4cxne6o1geyqa.png" alt=" " width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The ReportGenerator controls how the database is obtained.&lt;br&gt;
Under Inversion of Control, this responsibility is inverted.&lt;br&gt;
An external mechanism provides the dependency:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8xrc9socuhq118cysuxx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8xrc9socuhq118cysuxx.png" alt=" " width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now the framework or container controls object creation and dependency resolution.&lt;br&gt;
The high-level component declares what it needs but does not control how it is constructed.&lt;br&gt;
This inversion of control leads to looser coupling and greater flexibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  Constructor injection: the most common&amp;nbsp;form
&lt;/h2&gt;

&lt;p&gt;The most widely used form of dependency injection is constructor injection.&lt;br&gt;
Dependencies are passed through the constructor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5m13r2rqd2is94okfz6v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5m13r2rqd2is94okfz6v.png" alt=" " width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This approach has several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The object is fully initialized when created.&lt;/li&gt;
&lt;li&gt;Dependencies are clearly visible in the constructor signature.&lt;/li&gt;
&lt;li&gt;Fields can be immutable.&lt;/li&gt;
&lt;li&gt;It fails fast if a dependency is missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A class that requires certain dependencies should not be constructible without them.&lt;br&gt;
Constructor injection enforces this requirement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setter and interface injection
&lt;/h2&gt;

&lt;p&gt;Other forms of dependency injection exist, though they are less common in modern backend code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setter injection
&lt;/h3&gt;

&lt;p&gt;Dependencies are provided through setter methods after construction:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd1b62agsyyxgqlc4zvu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd1b62agsyyxgqlc4zvu.png" alt=" " width="790" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This can be useful when dependencies are optional or change over time, but it also allows objects to exist in a partially initialized state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interface injection
&lt;/h3&gt;

&lt;p&gt;A class implements an interface that defines how dependencies are injected.&lt;br&gt;
This is less common in TypeScript and JavaScript ecosystems.&lt;br&gt;
For most backend applications, constructor injection is the simplest and most reliable approach.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical NestJS&amp;nbsp;example
&lt;/h2&gt;

&lt;p&gt;NestJS provides a built-in dependency injection container.&lt;br&gt;
A service can declare its dependencies in the constructor:&lt;br&gt;
NestJS resolves the dependencies when the service is instantiated.&lt;br&gt;
The service does not need to know:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopfti2rockn6yc03423y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopfti2rockn6yc03423y.png" alt=" " width="800" height="792"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whether OrderRepository uses Prisma, TypeORM, or a raw SQL client.&lt;br&gt;
Whether PaymentGateway is implemented with Paystack, Stripe, or a mock.&lt;br&gt;
How NotificationService sends messages.&lt;/p&gt;

&lt;p&gt;It only knows the capabilities it requires.&lt;br&gt;
This separation makes the service easier to test and easier to evolve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why dependency injection matters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Reduced&amp;nbsp;coupling&lt;/strong&gt;&lt;br&gt;
When a class constructs its own dependencies, it is tightly coupled to those implementations.&lt;br&gt;
Dependency injection separates the use of a dependency from its construction.&lt;br&gt;
The class depends on an abstraction or interface, not on a specific concrete class.&lt;br&gt;
This makes it easier to switch implementations without modifying the consumer.&lt;br&gt;
For example, you can replace a payment gateway implementation without changing the order service.&lt;br&gt;
&lt;strong&gt;2. Improved testability&lt;/strong&gt;&lt;br&gt;
Testing becomes easier when dependencies can be replaced with fakes or mocks.&lt;br&gt;
Consider this test:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqaeeq9n2ify4wxuko62.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqaeeq9n2ify4wxuko62.png" alt=" " width="800" height="993"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The test can verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The correct amount is charged.&lt;/li&gt;
&lt;li&gt;The order is created with the right data.&lt;/li&gt;
&lt;li&gt;The notification is sent.&lt;/li&gt;
&lt;li&gt;Error handling behaves correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No real database, payment provider, or email server is required.&lt;br&gt;
This makes tests faster, more reliable, and easier to run in isolation.&lt;br&gt;
&lt;strong&gt;3. Easier configuration management&lt;/strong&gt;&lt;br&gt;
Different environments often require different configurations.&lt;br&gt;
For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Development may use local services.&lt;/li&gt;
&lt;li&gt;Staging may use test providers.&lt;/li&gt;
&lt;li&gt;Production may use real providers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With dependency injection, you can compose different implementations for different environments without changing the core business logic.&lt;br&gt;
&lt;strong&gt;4. Clearer responsibilities&lt;/strong&gt;&lt;br&gt;
When a class receives its dependencies, its constructor signature communicates what it needs.&lt;br&gt;
This makes the code easier to understand.&lt;br&gt;
A developer can look at the constructor and immediately see the external capabilities the class depends on.&lt;br&gt;
This is more explicit than hidden instantiations scattered throughout methods.&lt;br&gt;
&lt;strong&gt;5. Better separation of&amp;nbsp;concerns&lt;/strong&gt;&lt;br&gt;
Dependency injection helps separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business logic.&lt;/li&gt;
&lt;li&gt;Infrastructure details.&lt;/li&gt;
&lt;li&gt;Object construction.&lt;/li&gt;
&lt;li&gt;Configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The business logic focuses on what needs to happen.&lt;br&gt;
The infrastructure layer focuses on how to interact with external systems.&lt;br&gt;
The composition root or container focuses on how to assemble everything.&lt;br&gt;
This separation makes the system easier to reason about and easier to change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dependency injection versus service&amp;nbsp;locator
&lt;/h2&gt;

&lt;p&gt;A service locator is another pattern for obtaining dependencies.&lt;br&gt;
Instead of injecting dependencies, a class asks a central registry for them:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2tdpugy5qhk82yuqbme.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2tdpugy5qhk82yuqbme.png" alt=" " width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This may look convenient, but it has important drawbacks:&lt;br&gt;
Dependencies are hidden.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The class can request anything from the locator.&lt;/li&gt;
&lt;li&gt;It is harder to see what a class truly needs.&lt;/li&gt;
&lt;li&gt;Tests must configure the global locator.&lt;/li&gt;
&lt;li&gt;It can encourage tight coupling to the locator itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dependency injection makes dependencies explicit and visible in the constructor.&lt;br&gt;
This clarity is one of its main advantages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common mistakes with dependency injection
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Injecting concrete implementations everywhere
&lt;/h3&gt;

&lt;p&gt;Dependency injection is most powerful when classes depend on abstractions.&lt;br&gt;
If every service depends directly on concrete classes, you still have tight coupling, even if the container constructs them.&lt;br&gt;
For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffq8eq0k9s04s3zntitpo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffq8eq0k9s04s3zntitpo.png" alt=" " width="799" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is better than creating new PrismaClient() inside the service, but the service is still coupled to Prisma.&lt;br&gt;
A repository abstraction can help:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fodna0g1p3sbdtw4fbu4m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fodna0g1p3sbdtw4fbu4m.png" alt=" " width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now the service depends on a capability, not a specific library.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overusing dependency injection
&lt;/h3&gt;

&lt;p&gt;Not every function needs to be a class with injected dependencies.&lt;br&gt;
Simple utilities, pure functions, and small helpers may not benefit from dependency injection.&lt;br&gt;
For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwbv98dxmreh7yqa2wchi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwbv98dxmreh7yqa2wchi.png" alt=" " width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This function has no external dependencies.&lt;br&gt;
Wrapping it in a class and injecting it through a container adds unnecessary complexity.&lt;br&gt;
Use dependency injection where it solves a real problem: managing dependencies, improving testability, and reducing coupling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating too many tiny&amp;nbsp;services
&lt;/h3&gt;

&lt;p&gt;Dependency injection can encourage splitting behavior into many small services.&lt;br&gt;
This can be useful, but it can also create fragmentation.&lt;br&gt;
If understanding one workflow requires jumping through twenty classes, the design may be over-engineered.&lt;br&gt;
The goal is not maximum fragmentation.&lt;br&gt;
The goal is clear responsibilities and manageable boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ignoring the composition root
&lt;/h3&gt;

&lt;p&gt;In a NestJS application, the framework handles much of the composition.&lt;br&gt;
But in plain TypeScript or other environments, you need a place where the application is assembled.&lt;br&gt;
This is often called the composition root.&lt;/p&gt;

&lt;p&gt;It is where you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create concrete implementations.&lt;/li&gt;
&lt;li&gt;Configure providers.&lt;/li&gt;
&lt;li&gt;Wire dependencies together.&lt;/li&gt;
&lt;li&gt;Start the application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping this logic centralized makes the system easier to understand and easier to configure for different environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dependency injection and interfaces
&lt;/h2&gt;

&lt;p&gt;Dependency injection works especially well with interfaces or abstract classes.&lt;br&gt;
An interface defines a contract:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fch24oxwpslhgmvsmx7b1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fch24oxwpslhgmvsmx7b1.png" alt=" " width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Multiple implementations can satisfy the contract:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2nm66g5y3vm2j0223bl4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2nm66g5y3vm2j0223bl4.png" alt=" " width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The business logic depends on the interface:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujrsymwmydmp8iwptpr4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujrsymwmydmp8iwptpr4.png" alt=" " width="799" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This makes it easy to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switch providers.&lt;/li&gt;
&lt;li&gt;Test with a fake implementation.&lt;/li&gt;
&lt;li&gt;Introduce a new provider without modifying the checkout logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The class does not know how the payment is implemented.&lt;br&gt;
It only knows what it can ask the payment gateway to do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dependency injection in a multi-tenant SaaS
&lt;/h2&gt;

&lt;p&gt;In a multi-tenant SaaS platform, different tenants may require different configurations.&lt;br&gt;
For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Different payment providers.&lt;/li&gt;
&lt;li&gt;Different email providers.&lt;/li&gt;
&lt;li&gt;Different storage backends.&lt;/li&gt;
&lt;li&gt;Different feature flags.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dependency injection can help you compose tenant-specific implementations.&lt;br&gt;
A tenant context can determine which implementation to use:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4843d0hplu4caustrh3l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4843d0hplu4caustrh3l.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rest of the application depends on PaymentGateway, not on the specific provider.&lt;br&gt;
This keeps tenant-specific logic contained and makes the core application more stable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dependency injection and testing strategies
&lt;/h2&gt;

&lt;p&gt;Dependency injection enables several testing strategies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unit testing with&amp;nbsp;fakes
&lt;/h3&gt;

&lt;p&gt;You can provide simple in-memory or hardcoded implementations:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3zpal2a1eljx5wqcwoj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3zpal2a1eljx5wqcwoj.png" alt=" " width="799" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The service can be tested without a real database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unit testing with&amp;nbsp;mocks
&lt;/h3&gt;

&lt;p&gt;Mocking libraries can create objects that record how they were called:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foc95ij933636k74i63yv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foc95ij933636k74i63yv.png" alt=" " width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This verifies that the service interacts correctly with its dependencies.&lt;br&gt;
Integration testing with real dependencies&lt;br&gt;
For integration tests, you can use real or containerized dependencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A test database.&lt;/li&gt;
&lt;li&gt;A test message broker.&lt;/li&gt;
&lt;li&gt;A test email service or stub.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dependency injection makes it easier to swap between test and production configurations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dependency injection and clean architecture
&lt;/h2&gt;

&lt;p&gt;Clean architecture and hexagonal architecture emphasize separating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain logic.&lt;/li&gt;
&lt;li&gt;Application logic.&lt;/li&gt;
&lt;li&gt;Infrastructure.&lt;/li&gt;
&lt;li&gt;Interfaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dependency injection supports this separation by allowing inner layers to depend on abstractions while outer layers provide concrete implementations.&lt;br&gt;
For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feymcdy20h7w7pgv7waas.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feymcdy20h7w7pgv7waas.png" alt=" " width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application layer does not import infrastructure details.&lt;br&gt;
It only defines the capabilities it needs.&lt;br&gt;
This makes the system easier to evolve and easier to test.&lt;/p&gt;




&lt;h2&gt;
  
  
  When dependency injection may be&amp;nbsp;overkill
&lt;/h2&gt;

&lt;p&gt;Dependency injection is not required for every piece of code.&lt;br&gt;
It is most valuable when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A class has external dependencies.&lt;/li&gt;
&lt;li&gt;You need to test the class in isolation.&lt;/li&gt;
&lt;li&gt;Implementations may change.&lt;/li&gt;
&lt;li&gt;Configuration differs between environments.&lt;/li&gt;
&lt;li&gt;You want to reduce coupling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It may be unnecessary when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The code is a simple script.&lt;/li&gt;
&lt;li&gt;The function has no external dependencies.&lt;/li&gt;
&lt;li&gt;The behaviour is trivial and stable.&lt;/li&gt;
&lt;li&gt;The overhead outweighs the benefit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use dependency injection intentionally, not as a default ritual.&lt;/p&gt;




&lt;h2&gt;
  
  
  A simple mental&amp;nbsp;model
&lt;/h2&gt;

&lt;p&gt;You can think of dependency injection as answering three questions:&lt;br&gt;
&lt;strong&gt;What does this class&amp;nbsp;need?&lt;/strong&gt;&lt;br&gt;
Declared in the constructor.&lt;br&gt;
&lt;strong&gt;Who provides&amp;nbsp;it?&lt;/strong&gt;&lt;br&gt;
The container, factory, or composition root.&lt;br&gt;
&lt;strong&gt;How is it&amp;nbsp;built?&lt;/strong&gt;&lt;br&gt;
The concrete implementation, configuration, or environment.&lt;br&gt;
The class only answers the first question.&lt;br&gt;
The other questions are handled externally.&lt;br&gt;
This separation is what makes the code more flexible and easier to test.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Dependency injection is not only a framework feature.&lt;br&gt;
It is a design choice about who controls object creation and how dependencies are managed.&lt;br&gt;
When your classes know how to build everything they need, they become tightly coupled to those implementations.&lt;br&gt;
When your classes declare what they need and receive it from the outside, they become easier to test, easier to configure, and easier to change.&lt;br&gt;
This does not mean every function must be a class with injected dependencies.&lt;br&gt;
It means that when a component has external dependencies, those dependencies should be explicit, replaceable, and managed outside the component.&lt;br&gt;
Because good software is not only about making each class work.&lt;br&gt;
It is about designing a system where change does not require rewriting everything that depends on it.&lt;br&gt;
And dependency injection is one of the most practical tools for achieving that.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backenddevelopment</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>What Actually Happens When 10,000 Users Hit Your "Buy Now" Button at The Same Time</title>
      <dc:creator>Nsikan Patrick Adaowo</dc:creator>
      <pubDate>Mon, 03 Aug 2026 18:25:56 +0000</pubDate>
      <link>https://dev.to/nsikanadaowo/what-actually-happens-when-10000-users-hit-your-buy-now-button-at-the-same-time-5c18</link>
      <guid>https://dev.to/nsikanadaowo/what-actually-happens-when-10000-users-hit-your-buy-now-button-at-the-same-time-5c18</guid>
      <description>&lt;p&gt;Every e-commerce team dreams of massive traffic spikes—until those spikes actually happen.&lt;/p&gt;

&lt;p&gt;Imagine this: You’ve spent weeks (maybe months) building your e-commerce backend. Your API endpoints are clean, your database schema is normalized, and your authentication flow is solid. You test it with 10 users. It works. You test with 100. It still works.&lt;/p&gt;

&lt;p&gt;Then Black Friday hits. Or your product goes viral on TikTok. Or a celebrity tweet about your app. Suddenly, it’s no longer just one request (or even a hundred)—it’s 10,000, all landing on the same endpoint within the same second, all fighting over the same rows in your database.&lt;/p&gt;

&lt;p&gt;Your server slows to a crawl. Some users get errors. Others get charged twice. A lucky few get their orders, but the rest get nothing.&lt;/p&gt;

&lt;p&gt;This is the exact moment most engineers discover that “it works on my machine” and “it works under concurrency” are two completely different claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Went Wrong?
&lt;/h2&gt;

&lt;p&gt;This isn’t just a “traffic spike” problem. It’s a concurrency, architecture, and systems design problem. If you’re building scalable backends—especially in Node.js, NestJS, or any modern stack—you need to understand exactly what happens under the hood when thousands of requests hit your API simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mental Model: One Server, Thousands of Requests
&lt;/h2&gt;

&lt;p&gt;Let’s start with a simple question: Does your server create 10,000 copies of itself when 10,000 users hit it at once?&lt;/p&gt;

&lt;p&gt;No. It doesn’t.&lt;/p&gt;

&lt;p&gt;What actually happens is far more interesting—and more fragile.&lt;/p&gt;

&lt;p&gt;Your server has limited resources: CPU cores, memory, network bandwidth, and database connections. When a flood of requests arrives, your server doesn’t magically scale up. Instead, it tries to juggle those requests using the tools and patterns you’ve (hopefully) built into your architecture.&lt;/p&gt;

&lt;p&gt;In Node.js, for example, there’s only one main JavaScript thread—but that doesn’t mean it handles requests one at a time. It handles them concurrently, not in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency vs. Parallelism: The Core Distinction
&lt;/h2&gt;

&lt;p&gt;This is where most developers get tripped up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Concurrency&lt;/strong&gt; means multiple requests are in progress at the same time but not necessarily executing at the same time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Parallelism&lt;/strong&gt; means multiple requests are actually executing at the same time, on different CPU cores or threads.&lt;/p&gt;

&lt;p&gt;A common misconception about Node.js is that because it is single-threaded, high traffic will cause requests to pile up like cars stuck at a single toll booth.&lt;/p&gt;

&lt;p&gt;In reality, Node.js is single-threaded but highly concurrent. It uses an event loop to delegate I/O-bound tasks (like database queries, file reads, or external API calls) to the OS or thread pool, then moves on to the next request without waiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So, when 10,000 users hit your “Buy Now” endpoint:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Each request enters the event loop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the request involves a database query, Node.js offloads that work and immediately starts handling the next request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When the database responds, Node.js picks up where it left off and sends the response.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why Node.js can handle thousands of concurrent connections—even on a single thread.&lt;/p&gt;

&lt;p&gt;But here’s the catch: if your code blocks the event loop, everything grinds to a halt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Things Actually Break: The Bottlenecks
&lt;/h2&gt;

&lt;p&gt;Under heavy load, your API doesn’t just fail for no reason—it fails because of bottlenecks. Here are the most common ones:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Blocking the Event Loop&lt;/strong&gt; - If your code does something CPU-heavy in JavaScript—like encrypting passwords synchronously, parsing huge JSON payloads, or running complex calculations—it blocks the event loop. While that one request is hogging the thread, all other requests wait. This is why you should never use fs.readFileSync or heavy crypto in production code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Database Connection Pool Exhaustion&lt;/strong&gt; – Your database doesn’t have infinite connections. Most connection pools are configured to handle 10–100 concurrent connections. If 10,000 requests each try to open a new connection, your pool gets exhausted. Requests start queuing—or worse, failing with “too many connections” errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Unindexed or Slow Queries&lt;/strong&gt; – A single slow query can block hundreds of requests waiting on the same table. Under load, this becomes a cascade failure: one slow query → connection pool fills up → new requests timeout → users see errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- External API Calls Without Timeouts&lt;/strong&gt; – If your “Buy Now” endpoint calls a payment gateway, inventory service, or shipping API—and those calls don’t have timeouts—your server can get stuck waiting for responses that never come. Multiply that by 10,000 requests, and you’ve got a server meltdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Memory Pressure and Garbage Collection&lt;/strong&gt; – Under heavy load, your server allocates more memory. If you’re not careful, you can trigger frequent garbage collection pauses, which block the event loop and spike latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Threat: Race Conditions
&lt;/h2&gt;

&lt;p&gt;One of the most dangerous—and subtle—problems that emerges under high concurrency is the race condition. This occurs when two or more requests try to modify the same data simultaneously, and the final result depends on the unpredictable order in which the operations complete.&lt;/p&gt;

&lt;p&gt;Consider this classic e-commerce example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fllhwyqd6b8vyp9vt3h57.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fllhwyqd6b8vyp9vt3h57.png" alt=" " width="742" height="146"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without proper safeguards, two users can both be told an item is in stock, both complete checkout, and both be charged—even though only one unit was available. This is a race condition in its purest form, and it’s exactly why you need the strategies we’ll cover next.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, What Actually Happens When 10,000 Users Click “Buy Now”?
&lt;/h2&gt;

&lt;p&gt;Let’s walk through the sequence step by step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Requests Hit the Load Balancer Your traffic doesn’t hit a single server—it hits a load balancer (like AWS ALB, NGINX, or Cloudflare). The load balancer distributes requests across multiple backend instances. If you don’t have a load balancer, all 10,000 requests hit one server. That server will likely crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Each Request Enters the Event Loop On each backend instance (say, a Node.js server), requests enter the event loop. If your code is non-blocking, the server can handle thousands of concurrent requests per instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Database Queries Are Executed Each request tries to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Check inventory&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reserve the item&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Process payment&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create an order record&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your database isn’t optimized (no indexes, no connection pooling, no read replicas), this is where things slow down—and where race conditions can wreak havoc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Responses Are Sent Responses don’t go out in the order the requests arrived. They go out as soon as each request completes. Some users get their order confirmation in 200ms. Others wait 10 seconds. A few get timeouts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Design for 10,000 Concurrent Users
&lt;/h2&gt;

&lt;p&gt;You don’t need to over-engineer for day one—but you do need to design for scale. Here’s how:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Use Connection Pooling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Configure your database client (like pg for PostgreSQL or mysql2 for MySQL) to use a connection pool. This reuses connections instead of creating new ones for every request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4f4dw0i2yv2t9l7p98w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4f4dw0i2yv2t9l7p98w.png" alt=" " width="577" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Database-Level Locking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most direct fix lives at the database layer, using row-level locks. A SELECT ... FOR UPDATE statement tells the database: “Lock this specific row until my transaction finishes—nobody else can read or write it until I’m done.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdpzlj1y6iy406056l0i2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdpzlj1y6iy406056l0i2.png" alt=" " width="623" height="145"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Any other transaction trying to touch row 42 simply blocks until this one commits or rolls back. This guarantees correctness, but it introduces lock contention—under extreme concurrency, requests start queuing up waiting for locks, and latency climbs. For a single hyper-popular SKU, this can become a serious bottleneck even though the rest of your database is sitting idle.&lt;/p&gt;

&lt;p&gt;An alternative is optimistic locking, which skips the lock entirely and instead checks a version number at write time:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk56tuewg19j7joky1crw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk56tuewg19j7joky1crw.png" alt=" " width="637" height="105"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Decoupling with Queues&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Locking solves correctness at the cost of making requests wait synchronously. A more scalable pattern is to decouple the write from the request entirely using a message queue (Kafka, RabbitMQ, AWS SQS, Google Pub/Sub).&lt;/p&gt;

&lt;p&gt;Instead of your API handler directly touching the database:&lt;/p&gt;

&lt;p&gt;The incoming request is validated and dropped onto a queue as a message: { userId, productId, requestId }.&lt;/p&gt;

&lt;p&gt;The API immediately responds with something like 202 Accepted — processing.&lt;/p&gt;

&lt;p&gt;A pool of background workers consumes messages off the queue one at a time (or a few at a time) at a rate the database can actually sustain.&lt;/p&gt;

&lt;p&gt;Each worker executes the actual stock-check-and-deduct logic inside a transaction.&lt;/p&gt;

&lt;p&gt;The result (success/failure) gets pushed to the client via websocket, polling, or a notification.&lt;/p&gt;

&lt;p&gt;This converts a spike of 10,000 simultaneous writes into a controlled, sequential stream that the database can process without falling over. The tradeoff is eventual consistency—there’s a small delay between “I clicked buy” and “the system confirms it,” which needs to be communicated clearly in the UI (Spinners) so it doesn’t feel broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But here’s the reality check&lt;/strong&gt;: Would you really place all 10,000 requests into Kafka? Not necessarily. Because if you only have 100 units in stock, 99% of those requests are doomed to fail from the start. Dropping them into a queue only wastes worker capacity, memory, and network bandwidth.&lt;/p&gt;

&lt;p&gt;That’s why large-scale flash-sale systems move inventory protection closer to the edge. They preload inventory into a fast in-memory store (like Redis) and use it as a first gate. If Redis says stock = 0, the request is rejected immediately with a polite "Out of Stock" message, never even touching the queue or the primary database. The queue becomes a pathway only for requests that actually have a fighting chance, keeping your workers lean and your database unscathed.&lt;br&gt;
Add a Caching Layer&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Add a Caching Layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For read-heavy traffic—like thousands of users just checking if a product is in stock before deciding to buy—a cache (Redis, Memcached, or a CDN edge cache) intercepts most of that traffic before it ever reaches the database. Re-running the same read query 10,000 times a second against your primary database is pure waste.&lt;/p&gt;

&lt;p&gt;In high-concurrency “Buy Now” scenarios, Redis can go further and act as the first gate. Many flash-sale systems preload the available inventory into Redis at the start of the sale. When a request arrives, the first thing the API does is try an atomic decrement:&lt;/p&gt;

&lt;p&gt;DECR product:42:stock&lt;/p&gt;

&lt;p&gt;If the value was still positive, the request is allowed to proceed (and later confirmed in the database). If Redis returns a value less than zero, the request is rejected immediately with an “Out of Stock” response. It never reaches Kafka, never reaches the database, and never wastes a connection. This is extremely fast and protects the rest of your system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But… What if Redis crashes after decrementing the stock?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is why Redis should never be your source of truth. It’s a &lt;em&gt;traffic-shaping layer&lt;/em&gt; - an early filter that keeps the stampede from overwhelming the slower, more authoritative systems behind it, not the final authority. The database remains the single source of truth for inventory. If Redis goes down mid-sale, you lose your fast gate, but your database can still fall back on its own row-level locks and atomic updates to guarantee correctness.&lt;/p&gt;

&lt;p&gt;However, The classic hard problem remains: &lt;strong&gt;cache invalidation&lt;/strong&gt;. A stale cache can tell users an item is available when it just sold out. That’s why the Redis inventory numbers are usually treated as a temporary, short-lived view that is reconciled with the database on a short interval or after every successful write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Use Atomic Operations Instead of Read-Then-Write&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lot of race conditions disappear entirely if you avoid the "read, then write" pattern altogether and push the arithmetic into the database itself:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnghexbl7qbyy3q010upm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnghexbl7qbyy3q010upm.png" alt=" " width="591" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This single statement is &lt;strong&gt;atomic&lt;/strong&gt;. The database guarantees that the check and the decrement happen together. Suppose the last item has stock = 1 and two requests arrive at almost the same instant:&lt;/p&gt;

&lt;p&gt;Request A’s UPDATE succeeds. Stock becomes 0.&lt;br&gt;
Request B’s UPDATE finds stock &amp;gt; 0 is now false, so zero rows are updated. The request is rejected.&lt;/p&gt;

&lt;p&gt;No overselling. Only one transaction can successfully claim the final unit.&lt;br&gt;
That’s the pure inventory decrement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But here’s where it gets messy&lt;/strong&gt;. What if you decrement the stock successfully, but the payment gateway times out? Or the user's credit card declines? Or they close their browser mid-checkout?&lt;/p&gt;

&lt;p&gt;Imagine this: Stock is 1. User A reserves it. Your atomic UPDATE sets stock to 0. Then the payment fails. Now the product is completely unavailable—even though nobody actually bought it. You've just locked away your last item for a failed transaction. That's a terrible customer experience.&lt;/p&gt;

&lt;p&gt;Large e-commerce platforms solve this by separating reservation from purchase. Instead of permanently deducting stock at the start of checkout, inventory first moves into a temporary, time-bound reserved state. The stock stays reserved while the user completes payment, but it's not permanently gone yet.&lt;/p&gt;

&lt;p&gt;Only after a successful payment confirmation does the reservation convert into a permanent sale. If the payment fails, or if the user abandons the cart and walks away, the reservation simply expires—say, after 5-10 minutes—and the inventory is atomically released back into the available pool for the next customer.&lt;/p&gt;

&lt;p&gt;This two-phase approach adds a bit of complexity (you now need a background job to sweep expired reservations), but it prevents the heartbreaking scenario where a legitimate buyer is told "out of stock" because someone else's credit card just bounced. It turns your atomic operation from a blunt instrument into a surgical tool—and your users will notice the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Idempotency Keys&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Under load, clients retry. Networks flake, timeouts happen, users double-click. Without protection, this creates duplicate side effects—double charges, double stock deductions.&lt;/p&gt;

&lt;p&gt;The standard fix is an idempotency key: a unique identifier generated client-side (often a UUID) and attached to the request. The server stores which idempotency keys it has already processed and, on seeing a repeat, returns the cached original response instead of re-executing the logic:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5iedx7c0mwr0dqy5uft6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5iedx7c0mwr0dqy5uft6.png" alt=" " width="630" height="82"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stripe/Paystack’s API is the canonical real-world example of this pattern done well—it’s specifically designed so that retried payment requests never double-charge a customer, as long as the same idempotency key is reused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Rate Limiting and Backpressure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not every request deserves to reach your database at full speed. Rate limiting—using algorithms like token bucket or sliding window—caps how many requests per second a given user, IP, or API key can send, rejecting or delaying the rest with a 429 Too Many Requests response.&lt;/p&gt;

&lt;p&gt;This isn’t just about abuse prevention—it’s about protecting your own infrastructure. Related to this is backpressure: a signal that flows backward through a system telling upstream components “slow down, I can’t keep up,” so instead of a slow database causing your API servers to pile up thousands of stalled connections and crash, the system degrades gracefully by shedding or delaying excess load.&lt;/p&gt;

&lt;h2&gt;
  
  
  No Single Solution Solves the Entire Problem
&lt;/h2&gt;

&lt;p&gt;No single technique here solves the whole problem. Locking without queuing creates painful contention. Queuing without idempotency creates duplicate charges. Caching without invalidation lies to users. And none of this work if you haven’t addressed the underlying race conditions that concurrency exposes.&lt;/p&gt;

&lt;p&gt;It’s the combination—each one solving a different failure mode—that lets a system survive a genuine 10,000-request stampede without corrupting data or falling over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Why This Matters for You
&lt;/h2&gt;

&lt;p&gt;As a backend developer, your job isn’t just to write code that works. It’s to write code that works under pressure.&lt;/p&gt;

&lt;p&gt;When you understand what happens when 10,000 users hit your API, you stop thinking in terms of “my code” and start thinking in terms of systems. You ask:&lt;/p&gt;

&lt;p&gt;What happens if this endpoint gets 100x more traffic?&lt;/p&gt;

&lt;p&gt;Where are the bottlenecks?&lt;/p&gt;

&lt;p&gt;How do I fail gracefully?&lt;/p&gt;

&lt;p&gt;What metrics should I monitor?&lt;/p&gt;

&lt;p&gt;This mindset shift is what makes you valuable in remote job markets, freelance gigs, and high-growth startups.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>From Spaghetti to Structure: The Architecture Patterns Every Software Developer Should Know</title>
      <dc:creator>Nsikan Patrick Adaowo</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:31:00 +0000</pubDate>
      <link>https://dev.to/nsikanadaowo/from-spaghetti-to-structure-the-architecture-patterns-every-software-developer-should-know-2g9l</link>
      <guid>https://dev.to/nsikanadaowo/from-spaghetti-to-structure-the-architecture-patterns-every-software-developer-should-know-2g9l</guid>
      <description>&lt;p&gt;Ever inherited a codebase where changing one thing breaks five others?&lt;/p&gt;

&lt;p&gt;That’s rarely a coding problem. It’s an architecture one.&lt;/p&gt;

&lt;p&gt;When you first join a software team, code organization feels simple. You put database queries in one folder, business logic in another, and UI routes at the top. It feels clean and logical.&lt;/p&gt;

&lt;p&gt;Then the product hits real-world scale.&lt;/p&gt;

&lt;p&gt;Deadlines tighten. Business models pivot based on user behaviour. Features get tacked on hastily over frantic weekends. What started as an elegant codebase slowly evolves into a tangled, unpredictable web. &lt;br&gt;
Adding a simple feature to your checkout flow unexpectedly breaks an analytics pipeline three modules away. You realize you haven’t built a resilient application—you’ve built a house of cards.&lt;/p&gt;

&lt;p&gt;Most developers assume the fix is writing more utilities or refactoring syntax. But syntax doesn’t solve structural decay; architecture does.&lt;/p&gt;

&lt;p&gt;Software architecture patterns aren’t academic exercises or trivia for senior engineering interviews. They are time-tested blueprints designed to manage complexity, preserve developer velocity, and protect systems from falling apart when requirements change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here’s what every developer should understand:&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Cost of Spaghetti
&lt;/h2&gt;

&lt;p&gt;Spaghetti architecture doesn’t just make the code ugly. It makes the business slower. Features take longer. Bugs become more expensive. Good engineers start avoiding certain parts of the codebase. New hires take months instead of weeks to become productive. And eventually, someone suggests a full rewrite — which is usually just a more expensive way of making the same mistakes again.&lt;/p&gt;

&lt;p&gt;Most of this pain comes from missing or poorly enforced boundaries. When the UI can talk directly to the database, when domain logic lives inside controllers, when services depend on concrete implementations instead of abstractions, the system becomes a ball of mud. Change anything and the mud shifts.&lt;/p&gt;

&lt;p&gt;Architecture patterns are essentially agreements about where those boundaries should live and what is allowed to cross them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here is a definitive guide to the 10 core architectural patterns, broken down by how they actually function in production:&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Layered (N-Tier) Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Standard Horizontal Separation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Layered architecture is the default structure of almost every traditional backend framework. It divides an application horizontally into stacked tiers—typically Presentation, Business Logic, and Data Access—where each layer only talks to the one directly below it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- How it works in practice:&lt;/strong&gt;&lt;br&gt;
A user hits an endpoint, the Controller parses the request, hands it off to the Service layer to execute business rules, and the Service calls the Repository to write to the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- The Reality Check:&lt;/strong&gt;&lt;br&gt;
 While easy to learn, as your app grows, adding a single feature (like a user preference toggle) requires touching every single layer. Over time, layers become “pass-through tax boundaries” where code just forwards data downward without adding value.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Hexagonal Architecture (Ports &amp;amp; Adapters)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Isolating Core Business Logic from External Tech&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hexagonal Architecture (also known as ports and adapters) flips the traditional hierarchy inside out. Instead of placing the database at the bottom, it places your core domain logic at the exact center. The core defines abstract Ports (interfaces for what it needs—like PaymentGateway or UserRepository), and external technologies implement those ports using Adapters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- How it works in practice:&lt;/strong&gt;&lt;br&gt;
 Your core ordering logic doesn’t care if payments go through Paystack or Stripe, or if data lands in PostgreSQL or MongoDB. It only talks to its internal Port contracts. In other words, your application should not depend on external things. External things should depend on your application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- The Reality Check:&lt;/strong&gt;&lt;br&gt;
 You can test your entire business logic in milliseconds without spinning up a database or mocking complex HTTP calls. The trade-off is writing extra interface boilerplate up front.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Onion Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Concentric Rings of Dependency Control&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Coined by Jeffrey Palermo, Onion Architecture builds on the same principle as Hexagonal, visualization-wise using concentric circles. Domain Entities sit at the very core, surrounded by Domain Services, then Application Services, with Infrastructure and UI residing on the outermost ring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works in practice:&lt;/strong&gt; The strict rule of Onion Architecture is that dependencies only point inward. Outer rings depend on inner rings, but inner rings never know anything about outer rings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- The Reality Check:&lt;/strong&gt;&lt;br&gt;
It keeps your core domain pristine and framework-agnostic. However, engineers used to quick-and-dirty scripting might find navigating multiple concentric abstraction layers over-engineered for simple CRUD applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Clean Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Uncle Bob’s Unified Structural Blueprint&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Popularized by Robert C. Martin (”Uncle Bob”), Clean Architecture synthesizes Hexagonal and Onion principles into a single, standardized framework. It categorizes code into Entities (enterprise business rules), Use Cases (application business rules), Interface Adapters, and Frameworks/Drivers. The Dependency Rule is the key: source code dependencies can only point inward. Nothing in an inner circle can know about something in an outer circle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- How it works in practice:&lt;/strong&gt; Use Cases orchestrate the flow of data to and from Entities. Frameworks (like Next.js, NestJS, or Express) live in the outermost layer as disposable details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- The Reality Check:&lt;/strong&gt; It provides absolute clarity on where code belongs. The downside? It can lead to an explosion of files and mappings between DTOs (Data Transfer Objects) and Domain Entities for basic operations. Not every CRUD app needs full Clean Architecture ceremony. The value shows up when the business rules are complex and long-lived. If your domain logic is simple, a lighter approach often wins. The principle, however — protect the business rules from frameworks and delivery mechanisms — remains solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Vertical Slice Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Feature-First Autonomy Over Layered Overhead&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of slicing the app horizontally by technical concern (Controllers, Services, Repositories), Vertical Slice Architecture cuts vertically by business feature or use case (e.g., CreateOrder, CancelSubscription).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- How it works in practice:&lt;/strong&gt; Every feature slice is completely self-contained. It owns its endpoint, request handling, business logic, and database access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- The Reality Check:&lt;/strong&gt; It drastically reduces side-effect risks—modifying CreateOrder can never accidentally break CancelSubscription because they share no execution paths. It is arguably the most effective pattern for shipping products fast without technical debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Event-Driven Architecture (EDA)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Asynchronous Communication via Reactive Events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In an Event-Driven system, services don’t make direct, blocking calls to each other. Instead, when a state change occurs, a service emits an Event to a central broker (like Kafka or EventBridge). Other independent services subscribe to those events and react accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- How it works in practice:&lt;/strong&gt; When a user checks out, the Order Service emits OrderPlaced. The Inventory Service listens and reserves stock; the Notification Service listens and emails a receipt; the Analytics Service logs the event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- The Reality Check:&lt;/strong&gt; It decouples services almost entirely, allowing systems to handle massive asynchronous traffic spikes. The catch? Debugging distributed event chains and handling eventual consistency can be difficult without robust observability. But for many domains, the flexibility is worth the effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. CQRS (Command Query Responsibility Segregation)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Splitting Write Operations from Read Pipelines&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CQRS fundamentally splits an application’s data operations into two distinct pathways: Commands (writes that alter state) and Queries (reads that fetch data).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- How it works in practice:&lt;/strong&gt; The write side focuses heavily on business rules and transactional integrity (using a normalized database). The read side bypasses complex business logic entirely, reading pre-aggregated views from a fast search index or cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- The Reality Check:&lt;/strong&gt; It solves extreme performance bottlenecks when read traffic dwarfs write traffic (e.g., social media feeds, e-commerce catalogs). However, running two database models introduces temporary data lag (eventual consistency).&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Service-Oriented Architecture (SOA)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Enterprise-Wide Shared Business Capabilities&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Predating modern microservices, SOA is an architectural style where an enterprise splits its core systems into distinct, reusable services that communicate over a centralized communication bus—historically an Enterprise Service Bus (ESB).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- How it works in practice:&lt;/strong&gt; A centralized ESB handles message transformation, routing, and protocol conversion between large, enterprise-level services (like Billing, ERP, and CRM).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- The Reality Check:&lt;/strong&gt; SOA allowed massive enterprises to integrate disparate legacy systems. However, the central ESB often turned into a heavy, monolithic bottleneck managed by a single team.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Microservices Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Independently Deployable, Fine-Grained Services&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Microservices take the core idea of service decomposition and decentralize it completely. The application is built as a suite of small, autonomous services, each owning its own business domain, repository, and deployment pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- How it works in practice:&lt;/strong&gt; Instead of sharing a monolithic database, the User Service, Payment Service, and Catalog Service each run their own isolated databases and communicate over lightweight protocols (gRPC or REST).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- The Reality Check:&lt;/strong&gt; Microservices enable large organizations to scale engineering teams independently. But for small teams, they trade simple code problems for complex distributed network, deployment, and tracing problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- The promise is attractive:&lt;/strong&gt; independent deploy-ability, technology freedom, team autonomy, better scaling. But you trade a simple deployment problem for a distributed systems problem. Network latency, partial failures, eventual consistency, distributed transactions, observability, and operational complexity all showing up at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Serverless Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Function-as-a-Service &amp;amp; Managed Infrastructure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Serverless shifts the entire burden of server management, scaling, and provisioning to cloud providers. You write stateless granular functions (like AWS Lambda) that execute purely in response to triggers or HTTP requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- How it works in practice:&lt;/strong&gt; When an API route is called, the cloud provider spins up a lightweight execution environment, runs your function, returns the response, and immediately destroys or freezes the container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- The Reality Check:&lt;/strong&gt; You pay strictly for execution time down to the millisecond, and elasticity is virtually infinite out of the box. The main trade-offs are potential cold-start latencies and vendor lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, What Actually Matters in Practice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;- Boundaries beat cleverness:&lt;/strong&gt; Clear ownership and limited knowledge between parts of the system matter more than any specific pattern name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Start simple, evolve deliberately:&lt;/strong&gt; A modular monolith with good internal boundaries is often the best place to begin. Extract services or introduce events when the pain of not doing so becomes clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Patterns are tools, not identities:&lt;/strong&gt;&lt;br&gt;
 Saying “we do Clean Architecture” or “we are event-driven” is less useful than understanding the problems each pattern solves and the costs it introduces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Consistency of approach matters more than perfection:&lt;/strong&gt; A codebase that applies one set of principles reasonably well is usually healthier than one that mixes five different architectural styles without clear rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Architecture is a team sport:&lt;/strong&gt; The best patterns fail if the team doesn’t understand or respect the boundaries. Documentation, code reviews, and shared language matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Product Engineering Mindset: Architectural Balance
&lt;/h2&gt;

&lt;p&gt;It’s easy to get seduced by the distributed power of CQRS, Event-Driven processing, and Serverless. But every architectural pattern introduces trade-offs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Eventual Consistency:&lt;/strong&gt; Reads may lag behind writes by a few milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Operational Overhead:&lt;/strong&gt; Managing message brokers, dead-letter queues, and schema migrations requires real infrastructure discipline.&lt;/p&gt;

&lt;p&gt;A true product engineer doesn’t apply CQRS and Event-Driven systems everywhere. They isolate them to the specific 10% of their application that faces intense write contention or heavy async processing, while keeping the rest of the application simple.&lt;/p&gt;

&lt;p&gt;Scale where it counts, simplify where you can, and keep the user experience at the center of every architectural choice; because architecture isn’t about finding the “best” pattern; it’s about choosing the right set of trade-offs for your current scale.&lt;/p&gt;

</description>
      <category>designpatterns</category>
      <category>backenddevelopment</category>
      <category>eventdriven</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Your Code is a Reflection of Your Standards</title>
      <dc:creator>Nsikan Patrick Adaowo</dc:creator>
      <pubDate>Mon, 20 Jul 2026 22:27:34 +0000</pubDate>
      <link>https://dev.to/nsikanadaowo/your-code-is-a-reflection-of-your-standards-1ed5</link>
      <guid>https://dev.to/nsikanadaowo/your-code-is-a-reflection-of-your-standards-1ed5</guid>
      <description>&lt;p&gt;For a long time, I believed becoming a better developer was simply a matter of writing more code.&lt;br&gt;
The formula seemed straightforward and mechanical: Learn a language. Build a project. Complete a course. Move on to the next framework. Repeat. &lt;/p&gt;

&lt;p&gt;For a while, that high-velocity approach worked surprisingly well. Every new feature taught me something. Every bug forced me to dive into documentation files. Every tutorial introduced another technology that felt exciting.&lt;/p&gt;

&lt;p&gt;But as I began transitioning deeper into product engineering, I noticed a strange paradox.&lt;br&gt;
Although I was learning new tools constantly, I wasn't becoming dramatically better at solving real-world problems. I could spin up an application. I could create an API. I could design a relational database. Yet, whenever I looked at systems designed by experienced product engineers, their work felt fundamentally different.&lt;br&gt;
Not because it was more complex. Actually, it was usually much simpler.&lt;/p&gt;

&lt;p&gt;Their architecture was easier to reason about. Their projects had strategic restraint. Their decisions seemed completely intentional. Somehow, they weren't just writing software; they were designing an asset.&lt;/p&gt;

&lt;p&gt;For months, I assumed this clarity came from sheer years on the job. Eventually, I realized something else was happening. The senior engineers I admired didn't just look at documentation or scroll through technical blog posts. They read books that forced them to think deeply about system dynamics and human context.&lt;/p&gt;

&lt;p&gt;Right now, I am reading Clean Code by Robert C. Martin. It is a text many took for granted during the "vibe coding" boom, but in 2026, its principles have become an aggressive competitive advantage.&lt;/p&gt;

&lt;p&gt;Before picking up this book, my primary goal as a developer was shortsighted: Make the program work. &lt;/p&gt;

&lt;p&gt;If the application produced the correct output and the UI looked clean, I considered the task complete. I was focused on the "surface."&lt;br&gt;
Clean Code forces you to look at the "subsurface."&lt;/p&gt;

&lt;p&gt;Software rarely lives for a week or a month. In a startup environment, a product survives, evolves, and shifts over years. This means the code you ship today will be read, modified, and debugged far more often than it is written. Uncle Bob reminds us of a truth that every product engineer must internalize: Programming isn’t just communication with a compiler; it’s communication with people.&lt;/p&gt;

&lt;p&gt;One specific concept that shifted my mindset was the philosophy behind naming. On the surface, variable naming sounds trivial. The computer doesn’t care if a variable is named &lt;em&gt;t&lt;/em&gt; or &lt;em&gt;total&lt;/em&gt;. They produce identical machine code.&lt;br&gt;
But a product engineer doesn't build for the machine; they build for the team and the user. One variable forces future developers to guess; the other tells a story. &lt;/p&gt;

&lt;p&gt;When you have to touch a codebase six months later to pivot a feature based on customer feedback, clear naming is the difference between an elegant iteration and a broken system.&lt;/p&gt;

&lt;p&gt;Clean Code challenged my habits. Functions that stretched over hundreds of lines suddenly looked like architectural debt. Comments became a symptom of poor expression rather than a sign of good documentation. Complex conditional logic became something to simplify, not tolerate.&lt;/p&gt;

&lt;p&gt;As a product engineer, I’m glued already. This book has permanently changed how I review my own work. I no longer just ask, "Does this run?" I now ask, "Will another developer understand the intent behind this system without needing a meeting?"&lt;/p&gt;

&lt;p&gt;The tools and frameworks change every two months, but the fundamentals of building a sustainable system remain permanent. Clean Code isn't teaching you a tool; it’s training your judgment.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>cleancode</category>
    </item>
  </channel>
</rss>
