{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreic74djaziaf3srec66fqgrzyn42rgjrgh7fxs24qzmrmfc4k4lsby",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpijloguds72"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreia3wddiwhuckaw7weshez66pgigchea5kwfbvydsv35whp7lcozi4"
    },
    "mimeType": "image/webp",
    "size": 125510
  },
  "path": "/ahmed_niazy/how-does-a-senior-front-end-developer-think-11en",
  "publishedAt": "2026-06-30T07:48:57.000Z",
  "site": "https://dev.to",
  "tags": [
    "programming",
    "javascript"
  ],
  "textContent": "#  How Does a Senior Front-End Developer Think?\n\n##  Technical Decision-Making, Avoiding Overengineering, Building a Healthy Code Review Culture, and Managing Technical Debt\n\nAt the beginning of every front-end developer’s journey, most of the focus is placed on learning tools and technologies:\n\n  * How do I write JavaScript?\n  * How do I use React, Vue, or Angular?\n  * How do I consume APIs?\n  * How do I build reusable components?\n  * How do I manage application state?\n  * How do I create responsive layouts?\n\n\n\nThese questions are extremely important. They form the technical foundation that every front-end developer needs.\n\nHowever, after several years of professional experience, developers usually discover that writing code is not the most difficult part of software development.\n\nThe truly difficult parts are often:\n\n  * Making the right technical decision for a specific project.\n  * Choosing the appropriate level of complexity.\n  * Balancing delivery speed with code quality.\n  * Managing disagreements inside the engineering team.\n  * Knowing when a system needs refactoring.\n  * Knowing when existing code should be left alone.\n  * Managing technical debt before it becomes a serious problem.\n  * Protecting the project from overengineering.\n  * Conducting code reviews that improve the team rather than slow it down.\n\n\n\nThe real difference between a junior and a senior front-end developer is not that the senior developer knows more libraries or can write more complicated code.\n\nThe real difference is that a senior developer can make reasonable decisions in imperfect conditions.\n\nReal-world software development rarely happens inside an ideal project.\n\nIt happens inside projects that have:\n\n  * Tight deadlines.\n  * Limited budgets.\n  * Developers with different experience levels.\n  * Frequently changing requirements.\n  * Legacy code.\n  * Pressure from management or clients.\n  * Production incidents that must be fixed quickly.\n  * Real users who are affected by every mistake.\n\n\n\nThis article discusses four essential areas of senior front-end engineering:\n\n  1. How to make technical decisions.\n  2. How to avoid overengineering.\n  3. How to conduct effective code reviews.\n  4. How to manage technical debt.\n\n\n\n#  Part One: How Does a Senior Front-End Developer Make Technical Decisions?\n\n##  Technical decisions are not competitions for choosing the “best” technology\n\nOne of the most common mistakes in software development is treating technical decisions like competitions to identify the best technology available.\n\nTeams often ask questions such as:\n\n> What is the best state management library?\n>\n> What is the best front-end framework?\n>\n> Should we use micro-frontends?\n>\n> Should we build our own design system?\n\nThe problem is that these questions are usually incomplete.\n\nThere is no technology that is objectively best in every situation.\n\nThe better question is not:\n\n> What is the best technology?\n\nThe better question is:\n\n> Which technology is the most appropriate for this project, this team, this stage, and these constraints?\n\nRedux may be an excellent choice for one project and an unnecessary burden for another.\n\nNext.js may be a strong choice for a platform that heavily depends on search engine optimization and server rendering, but it may introduce unnecessary complexity into a private internal dashboard.\n\nMicro-frontends may be reasonable for an organization with dozens of independent engineering teams, but they may be a disastrous decision for a team of three developers.\n\nBuilding an internal design system may be a valuable long-term investment for a large platform, but it may be a waste of time for an MVP that must launch within four weeks.\n\nA technical decision should not be judged by how modern or impressive the technology looks.\n\nIt should be judged by how effectively it solves the real problem.\n\n#  Start with the problem, not the tool\n\nA less experienced developer may begin with a technology:\n\n> I want to use Zustand.\n>\n> I want to try GraphQL.\n>\n> I want to build the project using micro-frontends.\n>\n> I want to apply Clean Architecture everywhere.\n\nA more experienced developer begins with the problem:\n\n  * What problem are we trying to solve?\n  * Who is affected by it?\n  * What is the business impact?\n  * Is this a current problem or a hypothetical future problem?\n  * Do we have evidence that the problem exists?\n  * Do we need to solve it now?\n  * What is the simplest solution that can handle it?\n  * What will the solution cost the team?\n\n\n\nBefore proposing any technology, define the problem clearly.\n\nFor example, instead of saying:\n\n> We need Redux.\n\nSay:\n\n> The current user, permissions, shopping cart, and filters are being passed through several component levels, and managing them through props is becoming difficult.\n\nNow the problem is clear, and the team can compare multiple possible solutions:\n\n  * React Context.\n  * Zustand.\n  * Redux Toolkit.\n  * Component restructuring.\n  * Moving server state into React Query.\n  * Moving filters into the URL.\n  * Keeping some data local to the feature.\n\n\n\nThe team may discover that the actual problem is not the lack of a state management library.\n\nThe real problem may be that different categories of state are being mixed together.\n\n#  Types of technical decisions in front-end development\n\nA senior front-end developer makes technical decisions at several levels.\n\n##  1. Technology selection decisions\n\nExamples include:\n\n  * React, Vue, or Angular?\n  * TypeScript or JavaScript?\n  * Next.js or a client-side React application?\n  * REST or GraphQL?\n  * Redux, Zustand, or Context?\n  * Tailwind CSS, CSS Modules, or CSS-in-JS?\n  * An existing UI library or an internal design system?\n\n\n\n##  2. Architectural decisions\n\nExamples include:\n\n  * How should the project be structured?\n  * Should files be organized by technical type or by feature?\n  * Where should business logic live?\n  * How should the API layer be designed?\n  * How should errors be handled?\n  * How should authentication be implemented?\n  * How should authorization and permissions be represented?\n  * What are the boundaries of each module?\n\n\n\n##  3. Quality-related decisions\n\nExamples include:\n\n  * Which parts need automated tests?\n  * Which parts need unit tests?\n  * Which parts need integration tests?\n  * Do we need end-to-end tests?\n  * What linting rules should be enforced?\n  * What conditions must be met before a pull request can be merged?\n\n\n\n##  4. Performance-related decisions\n\nExamples include:\n\n  * Do we need code splitting?\n  * Do we need virtualization?\n  * Do we need memoization?\n  * Is the problem caused by rendering or networking?\n  * Do we need image optimization?\n  * Do we need a CDN?\n  * What caching strategy should be used?\n\n\n\n##  5. Delivery-related decisions\n\nExamples include:\n\n  * Should we build this component from scratch?\n  * Should we use an existing library?\n  * Should we implement the full solution or a simplified version?\n  * Is this the right time to refactor?\n  * Should some improvements be postponed until after the MVP launch?\n\n\n\nEvery one of these decisions has benefits, costs, and risks.\n\n#  The main factors behind a good technical decision\n\n##  1. Project size\n\nA solution that works for a small website may not be suitable for a large platform.\n\nImagine a simple landing page with five pages.\n\nDoes it really need:\n\n  * Redux?\n  * Micro-frontends?\n  * An event bus?\n  * A repository pattern?\n  * A domain layer?\n  * Complex dependency injection?\n  * A complete design system?\n\n\n\nProbably not.\n\nNow imagine a large platform with:\n\n  * Dozens of pages.\n  * Multiple roles and permissions.\n  * Several user types.\n  * Many business features.\n  * A large engineering team.\n  * External integrations.\n  * Real-time updates.\n  * Payments.\n  * Complex reporting.\n\n\n\nThat platform may require a higher level of structure, consistency, and architectural discipline.\n\nThe mistake is building a small application as if it were a global enterprise platform, or building a large platform using the same structure as a weekend side project.\n\n##  2. Team size\n\nTechnology selection depends not only on the system, but also on the people who will build and maintain it.\n\nIf the team consists of two developers, adding a highly sophisticated architecture may reduce productivity rather than improve it.\n\nIf the team consists of thirty developers, the absence of clear boundaries and conventions may lead to:\n\n  * Duplicated code.\n  * Inconsistent implementation styles.\n  * Conflicting responsibilities.\n  * Difficult code reviews.\n  * More defects.\n  * Slower feature delivery.\n\n\n\nIn a small team, many issues can be solved through direct communication.\n\nIn a large team, technical decisions must be documented and consistently applied.\n\nAs the team grows, the following become increasingly important:\n\n  * Naming conventions.\n  * Project structure.\n  * Shared components.\n  * Code review guidelines.\n  * Documentation.\n  * Testing strategy.\n  * Ownership.\n  * Continuous integration checks.\n\n\n\n##  3. Team experience\n\nA technology may be excellent in theory but unsuitable for a team that lacks the experience required to use it effectively.\n\nFor example, advanced functional programming may work well in a team that understands:\n\n  * Pure functions.\n  * Immutability.\n  * Function composition.\n  * Higher-order functions.\n  * Algebraic data types.\n\n\n\nHowever, if most of the team is unfamiliar with these concepts, applying them aggressively may produce code that is difficult to understand and maintain.\n\nThe same principle applies to:\n\n  * RxJS.\n  * GraphQL.\n  * Micro-frontends.\n  * Monorepos.\n  * WebAssembly.\n  * Advanced TypeScript types.\n  * Event-driven architecture.\n\n\n\nA successful technical solution must not only be understandable by the person who introduced it.\n\nIt must also be understandable, usable, and maintainable by the wider team.\n\nIt is easy to write code that makes you look intelligent.\n\nIt is much harder to write code that makes the whole team more productive.\n\n##  4. Maintainability\n\nCode is rarely written once and left untouched.\n\nIt is usually:\n\n  * Modified.\n  * Fixed.\n  * Extended.\n  * Reused.\n  * Moved.\n  * Read by new developers.\n  * Debugged under production pressure.\n\n\n\nTherefore, it is not enough to ask:\n\n> Does this solution work?\n\nWe should also ask:\n\n  * Will it still be understandable in six months?\n  * Can it be modified without breaking unrelated features?\n  * Are its responsibilities clear?\n  * Are there tests protecting critical behavior?\n  * Does it depend on a stable library?\n  * Does more than one person understand it?\n  * Will adding the next feature become easier or harder?\n\n\n\nSometimes the fastest solution to write becomes the most expensive solution to maintain.\n\nAt the same time, the most structured solution may be unjustified when the feature is simple, temporary, or unlikely to change.\n\nThe goal is not maximum abstraction.\n\nThe goal is an appropriate balance of clarity, flexibility, and simplicity.\n\n##  5. Performance\n\nPerformance decisions should not be based on assumptions.\n\nCommon statements include:\n\n> This approach is slower.\n>\n> We should use `useMemo`.\n>\n> We need to prevent re-renders.\n>\n> Everything should be lazy loaded.\n>\n> This library is too large.\n\nSome of these statements may be correct.\n\nOthers may have no meaningful effect on the user experience.\n\nPerformance should be measured.\n\nBefore implementing a performance optimization, ask:\n\n  * Where exactly is the problem?\n  * Is the bottleneck the network?\n  * Is the JavaScript bundle too large?\n  * Is rendering expensive?\n  * Are images poorly optimized?\n  * Is the API slow?\n  * Are there too many DOM nodes?\n  * Does the problem only appear on low-end devices?\n  * Which metric are we trying to improve?\n  * What is the value before and after the optimization?\n\n\n\nA senior developer does not optimize code simply because it looks theoretically imperfect.\n\nA senior developer optimizes when there is evidence of a real problem or a clearly predictable risk.\n\n##  6. Delivery time\n\nIn real projects, time is a critical factor.\n\nYou may have a perfect solution that needs three weeks and a good-enough solution that needs three days.\n\nIf the business needs to launch an early version to validate the market, the three-day solution may be the right decision.\n\nThis does not mean writing careless code.\n\nIt means selecting the right quality level for the current stage of the product.\n\nThere is a difference between:\n\n  * A deliberate temporary solution.\n  * Uncontrolled and careless code.\n  * A conscious shortcut used to test an idea.\n  * Completely ignoring quality.\n\n\n\nA reasonable decision might be:\n\n> We will use an existing library to launch quickly. If the feature proves valuable, we will replace it with an internal implementation later.\n\nThis is a rational trade-off because the team avoids investing heavily before confirming that the feature provides real value.\n\n##  7. The cost of complexity\n\nEvery new abstraction has a cost.\n\nEvery new layer has a cost.\n\nEvery new library has a cost.\n\nEvery new pattern has a cost.\n\nEvery service, factory, adapter, and manager has a cost.\n\nThe cost is not only the number of files.\n\nIt also includes:\n\n  * Learning time.\n  * Debugging difficulty.\n  * Cognitive load.\n  * Additional decisions.\n  * Onboarding difficulty.\n  * Future migration cost.\n  * Dependency on a small number of experts.\n\n\n\nComplexity must justify its existence.\n\nIf you add a new layer, it should solve a real problem.\n\nIf you add a library, its value should be greater than the cost of learning and maintaining it.\n\nIf you apply a pattern, it should make the code easier to understand or change.\n\nIt should not exist merely to demonstrate your knowledge of patterns.\n\n#  A practical framework for technical decision-making\n\nThe following process can help teams make important technical decisions.\n\n##  Step One: Define the problem\n\nWrite the problem in one clear sentence.\n\nFor example:\n\n> Server state management is duplicated across dozens of components using `useEffect` and `useState`, resulting in repeated loading, error, and caching logic.\n\nThis is better than saying:\n\n> We want to use React Query.\n\nReact Query is a possible solution.\n\nIt is not the problem itself.\n\n##  Step Two: Identify constraints\n\nPossible constraints include:\n\n  * Delivery deadline.\n  * Team size.\n  * Developer experience.\n  * Browser support.\n  * Performance requirements.\n  * SEO requirements.\n  * Product stability.\n  * Budget.\n  * Existing backend architecture.\n  * Available training time.\n\n\n\nConstraints can completely change the decision.\n\nChoosing a technology without understanding the constraints is like choosing a vehicle without knowing the road, the passengers, the cargo, or the budget.\n\n##  Step Three: Identify alternatives\n\nDo not enter the decision with only one option.\n\nFor a state management problem, possible alternatives may include:\n\n  * Component restructuring.\n  * React Context.\n  * Zustand.\n  * Redux Toolkit.\n  * React Query for server state.\n  * URL parameters for filters.\n  * Local feature state.\n\n\n\nThen document the advantages and disadvantages of each option.\n\n##  Step Four: Compare the trade-offs\n\nNo decision comes without trade-offs.\n\nRedux Toolkit may provide:\n\n  * Clear patterns.\n  * Strong debugging tools.\n  * Predictable state updates.\n  * Familiarity for large teams.\n\n\n\nHowever, it also introduces:\n\n  * Additional concepts.\n  * More boilerplate than lighter alternatives.\n  * Learning costs.\n  * The risk of putting unnecessary data into global state.\n\n\n\nZustand may be simpler and lighter, but without clear conventions it may allow application state to become disorganized.\n\nThe important point is not to present your preferred option as if it has no disadvantages.\n\nA senior developer communicates the cost of the solution alongside its benefits.\n\n##  Step Five: Choose the simplest solution that meets the real requirements\n\nThis does not mean choosing the simplest possible solution under all circumstances.\n\nIt means choosing the simplest solution that can support the current requirements and a reasonably expected future.\n\nThere is a major difference between:\n\n  * A future supported by an actual product roadmap.\n  * An imaginary future that might happen in five years.\n\n\n\nDo not build a complicated architecture because someone says:\n\n> We may need this in the future.\n\nAsk:\n\n  * Is it part of the roadmap?\n  * When is it expected?\n  * How likely is it to happen?\n  * What would it cost to change the solution later?\n  * Is adding complexity now really cheaper than adding it when needed?\n\n\n\n##  Step Six: Document the decision\n\nAn important technical decision should not live only inside a meeting or the memory of one developer.\n\nA simple Architecture Decision Record can contain:\n\n  * Decision title.\n  * Date.\n  * Problem description.\n  * Alternatives considered.\n  * Final decision.\n  * Reasons behind the decision.\n  * Accepted disadvantages.\n  * Conditions that would require reevaluation.\n\n\n\nExample:\n\n###  Decision\n\nUse React Query to manage server state.\n\n###  Reason\n\nData fetching, caching, loading states, and error handling are duplicated across many components. Most of the current global state is actually data retrieved from APIs.\n\n###  Alternatives considered\n\n  * Redux Toolkit.\n  * Custom hooks.\n  * Continuing with `useEffect` and `useState`.\n\n\n\n###  Accepted disadvantages\n\n  * Adding a new dependency.\n  * The team must learn query keys, invalidation, and cache behavior.\n\n\n\n###  Reevaluation conditions\n\nReevaluate the decision if the application becomes heavily real-time or develops requirements that the library does not support clearly.\n\nDocumentation reduces repeated debates and helps new team members understand why the system was designed in a particular way.\n\n#  Part Two: When Clean Code Becomes Unnecessary Complexity\n\n##  What is overengineering?\n\nOverengineering means building a solution that is more complicated than the problem requires.\n\nIt can appear in many forms:\n\n  * Creating ten layers for a simple operation.\n  * Building a generic solution for a single use case.\n  * Applying a design pattern without a real need.\n  * Building a plugin architecture without plugins.\n  * Creating a complete design system for a small project.\n  * Using micro-frontends with a small team.\n  * Adding state management for every value.\n  * Converting every function into a class.\n  * Building abstractions before any real duplication appears.\n  * Designing for highly unlikely future requirements.\n  * Adding large numbers of low-value tests.\n  * Rewriting working code because it does not match an idealized architecture.\n\n\n\nThe danger of overengineering is that it often looks professional.\n\nYou may see:\n\n  * Sophisticated terminology.\n  * Many interfaces.\n  * Several layers.\n  * Popular patterns.\n  * Complex generic types.\n  * Extremely small files.\n  * Numerous abstractions.\n\n\n\nThe project may look organized, but in reality it may be harder to understand and modify.\n\n#  Example: overengineering a simple API request\n\nImagine a page that needs to load user data.\n\nA simple implementation may look like this:\n\n\n\n    export async function getUser(userId: string): Promise<User> {\n      const response = await fetch(`/api/users/${userId}`);\n\n      if (!response.ok) {\n        throw new Error(\"Failed to load user\");\n      }\n\n      return response.json();\n    }\n\n\nA developer may decide to create:\n\n  * `UserApiClient`\n  * `UserRepositoryInterface`\n  * `HttpClientInterface`\n  * `FetchHttpClient`\n  * `UserRepositoryImplementation`\n  * `UserService`\n  * `UserMapper`\n  * `UserDTO`\n  * `UserEntity`\n  * `GetUserUseCase`\n  * `UserServiceFactory`\n\n\n\nIs this always wrong?\n\nNo.\n\nIt may be justified in a large system with:\n\n  * Multiple data sources.\n  * Offline support.\n  * A complex domain.\n  * A requirement to replace the transport layer.\n  * Independent domain testing.\n  * Large teams and strict boundaries.\n\n\n\nHowever, in a small dashboard with one API provider, this may be complexity without sufficient value.\n\nInstead of opening one file to understand how the user is loaded, a developer may need to navigate through ten layers to find where the user’s name comes from.\n\n#  Premature abstraction\n\nOne of the most common causes of overengineering is creating abstractions before fully understanding the problem.\n\nA developer sees two similar lines and immediately creates a generic utility.\n\nA developer sees two similar components and merges them into a single reusable component with many configuration options.\n\nA developer sees two forms and builds a dynamic form engine.\n\nA developer sees two tables and builds a universal table component.\n\nA developer sees two modal windows and builds a modal framework capable of handling every possible scenario.\n\nOver time, the supposedly reusable component may look like this:\n\n\n\n    <DataTable\n      enableSelection\n      disableSelectionOnMobile\n      enableExpandableRows\n      disableExpansionForDisabledItems\n      useRemotePagination\n      enableStickyHeader\n      renderCustomHeader\n      useLegacySorting\n      preserveQueryState\n      enableConditionalActions\n    />\n\n\nAt this point, the team no longer has a reusable component.\n\nIt has created an internal framework that requires its own documentation, testing, and maintenance.\n\n#  The Rule of Three\n\nA useful guideline for avoiding premature abstraction is the Rule of Three:\n\n> Do not extract an abstraction at the first sign of duplication. Wait until the real pattern becomes clear.\n\nA common interpretation is:\n\n  * The first time: write the solution.\n  * The second time: notice the similarity.\n  * The third time: evaluate whether abstraction is justified.\n\n\n\nThis is not a strict law, but it is an important reminder that temporary duplication may be cheaper than an incorrect abstraction.\n\nDuplication is not always the worst outcome.\n\nSometimes two clear implementations are better than one highly configurable component filled with conditions.\n\n#  When reuse becomes harmful\n\nReuse becomes harmful when it connects pieces of code that change for different reasons.\n\nImagine that you have:\n\n  * A product card.\n  * An employee card.\n\n\n\nBoth initially contain:\n\n  * An image.\n  * A title.\n  * A description.\n  * A button.\n\n\n\nIt may seem reasonable to create a generic card component.\n\nHowever, the product card may later require:\n\n  * Price.\n  * Discount.\n  * Stock status.\n  * Rating.\n  * Add-to-cart behavior.\n\n\n\nThe employee card may later require:\n\n  * Job title.\n  * Department.\n  * Employment status.\n  * Contact information.\n  * Profile actions.\n\n\n\nAlthough the components initially looked similar, they belong to different domains and evolve for different reasons.\n\nForcing them into a single abstraction may produce a component filled with conditional logic.\n\nA better principle is:\n\n> Do not create reuse based only on visual similarity. Reuse should be based on shared responsibility and shared reasons for change.\n\n#  Signs of overengineering\n\n##  1. Simple flows are difficult to trace\n\nIf understanding a button click requires opening a large number of files, the project may have too many layers.\n\n##  2. Many files contain only one or two meaningful lines\n\nFile separation is not a goal by itself.\n\n##  3. Many interfaces have only one implementation\n\nInterfaces are not inherently bad, but they should have a reason, such as:\n\n  * Multiple implementations.\n  * Clear module boundaries.\n  * Testability.\n  * Isolation from an external dependency.\n\n\n\n##  4. Generic components contain too many conditions\n\nA growing number of Boolean properties is often a sign that the component owns too many responsibilities.\n\n##  5. Simple features require long explanations\n\nIf a new developer needs an entire day to understand where to add one field, something may be wrong.\n\n##  6. The system solves problems that have not happened\n\nExamples include:\n\n  * Supporting five backend providers when only one exists.\n  * Supporting ten themes when only one is planned.\n  * Building a plugin system without plugins.\n  * Supporting several languages in a single-language internal tool without a roadmap for localization.\n\n\n\n##  7. Small changes are unexpectedly expensive\n\nIf changing a label requires modifying six layers, the abstraction has become a burden.\n\n#  How to avoid overengineering\n\n##  1. Start with the direct solution\n\nBegin with the simplest clear implementation.\n\nDo not begin with an imagined final architecture.\n\nStart with what the feature needs today, and allow the architecture to evolve as the team understands the problem better.\n\n##  2. Add complexity gradually\n\nAn API layer may begin like this:\n\n\n\n    export async function createOrder(payload: CreateOrderPayload) {\n      return api.post(\"/orders\", payload);\n    }\n\n\nAs real needs appear, the team may add:\n\n  * Validation.\n  * Data mapping.\n  * Retry logic.\n  * Caching.\n  * Error normalization.\n  * Logging.\n  * A repository layer.\n\n\n\nThere is no need to add everything on the first day.\n\n##  3. Do not apply a pattern simply because you recently learned it\n\nAfter learning design patterns, developers often feel motivated to use them everywhere.\n\nA pattern is not a badge that proves experience.\n\nIt is a common solution to a common problem.\n\nIf the problem does not exist, the solution is unnecessary.\n\n##  4. Prefer readable code over clever code\n\nThe following implementation may look concise and sophisticated:\n\n\n\n    const result = items.reduce(\n      (acc, item) => ({\n        ...acc,\n        [item.type]: [...(acc[item.type] ?? []), item],\n      }),\n      {}\n    );\n\n\nHowever, it may be less readable for the team and may also create unnecessary objects.\n\nA more direct version may be easier to understand:\n\n\n\n    const result: Record<string, Item[]> = {};\n\n    for (const item of items) {\n      if (!result[item.type]) {\n        result[item.type] = [];\n      }\n\n      result[item.type].push(item);\n    }\n\n\nThe goal is not to use the shortest syntax.\n\nThe goal is to reduce the mental effort required to understand the code.\n\n##  5. Monitor cognitive load\n\nEvery feature should have a reasonable cognitive cost.\n\nIf a small change requires understanding:\n\n  * An event bus.\n  * Dependency injection.\n  * The repository pattern.\n  * The command pattern.\n  * A state machine.\n  * Custom middleware.\n  * A custom internal framework.\n\n\n\nThe team should ask whether all these concepts are truly necessary.\n\n#  Part Three: How to Conduct Code Reviews Without Creating Conflict\n\n##  Code review is not a trial\n\nCode review is not a place to prove who is the strongest developer.\n\nIt is not an examination of the pull request author’s intelligence.\n\nIt is not an opportunity to rewrite the implementation according to the reviewer’s personal style.\n\nThe purpose of code review is to:\n\n  * Protect system quality.\n  * Discover defects.\n  * Share knowledge.\n  * Improve readability.\n  * Reduce risk.\n  * Maintain consistency.\n  * Develop team skills.\n  * Confirm that the implementation meets business requirements.\n\n\n\nWhen code review becomes personal conflict, the team loses one of its most valuable quality practices.\n\n#  What should be reviewed?\n\nCode review should not focus only on syntax.\n\nIt should evaluate several dimensions.\n\n##  1. Correct behavior\n\nAsk:\n\n  * Does the code meet the requirement?\n  * Are important edge cases handled?\n  * What happens when the API fails?\n  * What happens on a slow connection?\n  * What happens when the data is empty?\n  * What happens if the user clicks twice?\n  * Are permissions enforced?\n  * Is there a race condition?\n\n\n\n##  2. Readability\n\nAsk:\n\n  * Are names clear?\n  * Does each function have one understandable responsibility?\n  * Is there unnecessary complexity?\n  * Can the flow be followed easily?\n  * Do comments explain important reasons?\n  * Are comments merely repeating the code?\n\n\n\n##  3. Architecture\n\nAsk:\n\n  * Is the code placed in the right module?\n  * Does the component contain too much business logic?\n  * Is there unnecessary coupling?\n  * Does the change violate module boundaries?\n  * Is existing logic being duplicated?\n  * Does the solution align with the project’s architecture?\n\n\n\n##  4. Performance\n\nAsk:\n\n  * Are there unnecessary renders?\n  * Is data requested more than once?\n  * Is a large list rendered without virtualization?\n  * Are expensive operations performed during rendering?\n  * Are images properly optimized?\n  * Is optimization actually required?\n\n\n\n##  5. Security\n\nAsk:\n\n  * Is sensitive information exposed in the front end?\n  * Is the implementation relying on hiding a button instead of backend authorization?\n  * Is HTML being injected unsafely?\n  * Is the authentication token stored dangerously?\n  * Are logs exposing private information?\n\n\n\n##  6. Testing\n\nAsk:\n\n  * Are critical behaviors protected by tests?\n  * Do tests validate behavior rather than implementation details?\n  * Are important edge cases covered?\n  * Are the tests stable and understandable?\n\n\n\n#  How to write a useful code review comment\n\n##  Explain the reason\n\nA weak comment:\n\n> Change this.\n\nA stronger comment:\n\n> Consider moving the pricing calculation outside the component because it represents business logic. Keeping it here will make it harder to reuse and test independently.\n\nThe second comment explains the reason and helps the author make better decisions in the future.\n\n##  Separate blockers from suggestions\n\nNot every comment has the same importance.\n\nTeams may use labels such as:\n\n  * **Blocker:** Must be fixed before merging.\n  * **Important:** Significant issue that should be discussed.\n  * **Suggestion:** Optional improvement.\n  * **Nitpick:** Minor stylistic observation.\n  * **Question:** Request for clarification.\n  * **Praise:** Positive feedback on a strong decision.\n\n\n\nExample:\n\n> Suggestion: We could move this logic into a custom hook to reduce the component’s responsibilities, but the current implementation does not need to block the merge.\n\nThis prevents the pull request author from treating every comment as a mandatory order.\n\n##  Ask before assuming\n\nInstead of saying:\n\n> This is wrong. Use Context.\n\nAsk:\n\n> Was there a reason for passing this data through four component levels instead of using Context? Are we intentionally keeping the dependency local to the feature?\n\nThe author may have a valid reason that is not immediately visible.\n\nQuestions open conversations.\n\nJudgments often close them.\n\n##  Discuss the code, not the person\n\nAn unhealthy comment:\n\n> You always make things too complicated.\n\nA healthy comment:\n\n> This abstraction adds several levels of indirection for a single use case. Could we start with a direct implementation and extract the abstraction if a second case appears?\n\nDo not connect code quality with a person’s intelligence or competence.\n\nCode can be changed.\n\nPersonal attacks damage trust within the team.\n\n##  Do not turn code review into personal preference\n\nThere is a difference between:\n\n  * A real defect.\n  * A violation of an agreed team standard.\n  * A personal preference.\n\n\n\nFor example:\n\n\n\n    if (!user) return null;\n\n\nCompared with:\n\n\n\n    if (user === null) {\n      return null;\n    }\n\n\nIf the project has no explicit rule, a pull request should not be blocked because of an individual preference.\n\nTools should handle decisions that can be automated:\n\n  * ESLint.\n  * Prettier.\n  * TypeScript.\n  * Automated tests.\n\n\n\nMachines should handle formatting and deterministic rules.\n\nHuman review should focus on behavior, architecture, risk, and clarity.\n\n#  The responsibility of the pull request author\n\nCode review is a shared responsibility.\n\nThe author should make the change easy to review.\n\nA useful pull request should contain:\n\n  * A clear title.\n  * A description of the problem.\n  * A summary of the solution.\n  * Screenshots or videos for UI changes.\n  * Testing instructions.\n  * Known risks.\n  * Decisions that require discussion.\n  * A link to the task or requirement.\n  * A clear explanation of what is outside the scope.\n\n\n\n#  Keep pull requests as small as reasonably possible\n\nReviewing 300 lines is easier than reviewing 4,000 lines.\n\nVery large pull requests often lead to:\n\n  * Superficial reviews.\n  * Loss of focus.\n  * Missed defects.\n  * Difficult testing.\n  * Difficult rollback.\n  * Slow merging.\n  * More merge conflicts.\n\n\n\nThis does not mean splitting changes artificially.\n\nIt means dividing work into logical, reviewable units.\n\nFor example:\n\n  1. Introduce the API client.\n  2. Add state management.\n  3. Add the user interface.\n  4. Add tests.\n  5. Enable the feature.\n\n\n\n#  How should teams handle disagreement?\n\n##  Return to the goal\n\nWhen two solutions compete, ask:\n\n  * Which solution is clearer?\n  * Which solution has less risk?\n  * Which solution is easier to maintain?\n  * Do we already have an established pattern?\n  * What are the performance requirements?\n  * What are the time constraints?\n  * Can we measure the outcome?\n  * Is the decision reversible?\n\n\n\nThe discussion should not be:\n\n> My way is better.\n\nIt should be:\n\n> Which option serves the project more effectively?\n\n##  Use small experiments\n\nIf the disagreement concerns performance, measure both approaches.\n\nIf the disagreement concerns a library, build a small proof of concept.\n\nIf the disagreement concerns architecture, test it on a limited feature.\n\nExperiments reduce theoretical debates.\n\n##  Define a decision owner\n\nThe team does not need complete agreement on every decision.\n\nIt should be clear who has final decision authority:\n\n  * The technical lead.\n  * The front-end lead.\n  * The feature owner.\n  * The architecture group.\n\n\n\nAfter listening to the available perspectives, a decision should be made and the team should commit to it unless new information appears.\n\nEndless debate is often more damaging than making a reasonable but imperfect decision.\n\n#  Part Four: Technical Debt in Front-End Development\n\n##  What is technical debt?\n\nTechnical debt is the future cost created by choosing a faster, lower-quality, or less flexible solution today.\n\nIt is similar to financial debt.\n\nYou receive an immediate benefit, but you may pay additional cost later.\n\nImagine that a feature must launch in two days.\n\nThe team adds validation directly inside a component instead of building a clean validation layer.\n\nThat decision may be acceptable.\n\nHowever, the project now owes several future improvements:\n\n  * Move validation logic into a better location.\n  * Add tests.\n  * Standardize error messages.\n  * Remove duplication.\n\n\n\nThe problem is not the existence of technical debt.\n\nThe problem is ignoring it, failing to understand its size, or allowing it to grow without control.\n\n#  Types of technical debt\n\n##  1. Deliberate technical debt\n\nThe team knowingly chooses a temporary solution because of:\n\n  * A deadline.\n  * An experiment.\n  * A production incident.\n  * A high-priority customer.\n  * Unclear requirements.\n\n\n\nThis may be reasonable when it is documented.\n\nExample:\n\n> We will keep the filters inside the component for the first version. If the feature is approved, we will move them into URL state.\n\n##  2. Accidental technical debt\n\nThis happens because of:\n\n  * Limited experience.\n  * Misunderstood requirements.\n  * Weak review.\n  * An unsuitable architecture.\n  * Incorrect use of technology.\n  * Missing tests.\n\n\n\nThis form is more dangerous because the team may not know it exists.\n\n##  3. Debt caused by system evolution\n\nCode may have been well-designed when it was written, but the system changed.\n\nFor example:\n\n  * The system had one user type and now has five.\n  * The application supported one country and now supports several.\n  * Pricing was fixed and now includes taxes and currencies.\n  * A component was used once and is now used in twenty places.\n  * An API was simple and now supports pagination, caching, and real-time updates.\n\n\n\nOld code is not automatically bad.\n\nSometimes the requirements simply outgrow the original design.\n\n##  4. Tooling and dependency debt\n\nExamples include:\n\n  * An outdated React version.\n  * Old dependencies.\n  * A legacy build tool.\n  * An unmaintained library.\n  * Slow test suites.\n  * Unstable CI/CD pipelines.\n  * Weak TypeScript configuration.\n  * Missing error monitoring.\n\n\n\nUsers may not directly see this debt, but it increases the cost of every new feature.\n\n#  Common technical debt in front-end systems\n\nExamples include:\n\n  * Components with thousands of lines.\n  * Repeated API logic across many files.\n  * Excessive use of `any`.\n  * Disorganized global state.\n  * Direct use of local storage everywhere.\n  * No consistent error handling.\n  * Difficult and highly coupled CSS.\n  * Missing design tokens.\n  * Inconsistent components across pages.\n  * Missing tests for critical flows.\n  * Outdated dependencies.\n  * Ignored console warnings.\n  * Using array indexes as keys in reorderable lists.\n  * Race conditions in search and filtering.\n  * Memory leaks caused by listeners or subscriptions.\n  * Requests that are never cancelled.\n  * Files that may no longer be used.\n  * Old feature flags that were never removed.\n  * Workarounds whose original reason no longer exists.\n  * Authorization logic scattered throughout the UI.\n\n\n\n#  When is technical debt acceptable?\n\nTechnical debt may be acceptable when:\n\n  * The team is aware of it.\n  * The reason is clear.\n  * The future cost is understood.\n  * The temporary solution has limited scope.\n  * There is a plan for repayment.\n  * The solution can be replaced without extreme difficulty.\n  * Security is not compromised.\n  * Data integrity is not threatened.\n  * Operational risk remains controlled.\n\n\n\nA reasonable example:\n\n> For the MVP, we will use an existing UI library rather than build a design system. If the product succeeds and reaches a specific number of screens, we will gradually introduce design tokens and internal components.\n\nThis is a conscious decision.\n\n#  When does technical debt become dangerous?\n\nTechnical debt becomes dangerous when:\n\n  * Every feature takes longer than the previous one.\n  * Fixing one area breaks unrelated areas.\n  * Developers are afraid to modify the code.\n  * No one fully understands the system.\n  * The same defects keep returning.\n  * Debugging time continuously increases.\n  * Team velocity decreases even as the team grows.\n  * Tests are unreliable.\n  * Deployments become stressful.\n  * Production defects increase.\n  * New developer onboarding takes too long.\n  * Most engineering time is spent on workarounds.\n  * Adding a simple field requires changes in many unrelated files.\n\n\n\nAt this stage, technical debt becomes a tax that the team pays on every task.\n\n#  How to manage technical debt\n\n##  1. Make the debt visible\n\nDo not leave important debt inside comments or developers’ memories.\n\nMaintain a technical debt register containing:\n\n  * Problem description.\n  * Business and technical impact.\n  * Severity.\n  * Affected areas.\n  * Suggested solution.\n  * Rough estimate.\n  * Reason for postponement.\n  * Reevaluation date.\n\n\n\nNot every `TODO` comment represents managed technical debt.\n\nImportant debt should be visible to both engineering and product management.\n\n##  2. Classify debt by impact\n\nA possible classification is:\n\n###  Critical\n\n  * A security vulnerability.\n  * Risk of data loss.\n  * Unsafe dependencies.\n  * Code causing production outages.\n\n\n\n###  High\n\n  * Code that blocks important feature development.\n  * A central component that is extremely difficult to modify.\n  * Unstable tests that delay releases.\n  * Architecture that repeatedly causes defects.\n\n\n\n###  Medium\n\n  * Significant duplication.\n  * Weak naming.\n  * Incomplete type safety.\n  * Areas that need refactoring.\n\n\n\n###  Low\n\n  * Cosmetic cleanup.\n  * File reorganization.\n  * Simplification opportunities with little immediate impact.\n\n\n\nThis prevents teams from treating all technical debt as equally urgent.\n\n##  3. Connect technical debt to business impact\n\nSaying:\n\n> The code is not clean.\n\nMay not convince business stakeholders.\n\nA stronger explanation would be:\n\n> Adding a new payment method currently requires changes in seven components, and the current structure caused three defects last month. Refactoring the payment flow could reduce the implementation time for the next payment provider from five days to two.\n\nThis translates the problem into:\n\n  * Time.\n  * Cost.\n  * Risk.\n  * Delivery speed.\n  * User experience.\n  * System reliability.\n\n\n\nTechnical improvements become easier to prioritize when their business impact is clear.\n\n##  4. Repay debt gradually\n\nThe team does not always need to stop product work for three months and rebuild everything.\n\nThe Boy Scout Rule is useful:\n\n> Leave the code slightly better than you found it.\n\nWhen working on a feature, you can:\n\n  * Improve an unclear name.\n  * Add a test for the area you are changing.\n  * Extract duplicated logic.\n  * Remove dead code.\n  * Fix a weak type.\n  * Reduce component responsibility.\n  * Improve error handling.\n\n\n\nSmall continuous improvements are often more realistic than waiting for a perfect rewrite that may never happen.\n\n##  5. Allocate regular time\n\nA team can reserve part of each sprint for technical improvements.\n\nFor example:\n\n  * Ten to twenty percent of development capacity.\n  * One technical improvement task per sprint.\n  * A monthly maintenance day.\n  * Refactoring tied to upcoming feature work.\n  * Debt cleanup in the area currently being modified.\n\n\n\nThe right percentage depends on the health of the product.\n\nA new project may need less.\n\nAn unstable legacy project may need significantly more.\n\n#  Should we rewrite the entire application?\n\nA complete rewrite can be very attractive.\n\nTeams often say:\n\n> The current codebase is terrible. We should start again.\n\nHowever, full rewrites carry serious risks:\n\n  * Hidden business behavior may be lost.\n  * Previously solved defects may return.\n  * New feature development may stop for a long period.\n  * Requirements may change during the rewrite.\n  * The new system may become complicated as well.\n  * Old and new systems may need to run together.\n  * Completion may be difficult to define.\n\n\n\nA rewrite may be correct in certain situations, but it should not be the default answer.\n\n#  When may a rewrite be justified?\n\nA rewrite may be reasonable when:\n\n  * The current technology is no longer supported.\n  * The architecture blocks essential new requirements.\n  * Maintenance cost is clearly higher than replacement cost.\n  * There are fundamental security or operational problems.\n  * Migration can be divided into manageable stages.\n  * The team understands the behavior of the existing system.\n  * There is a realistic migration plan.\n\n\n\n#  The alternative: incremental modernization\n\nThe Strangler Pattern can be used to replace a system gradually:\n\n  * Build the new part beside the old part.\n  * Migrate one feature at a time.\n  * Create clear boundaries between the systems.\n  * Move users or workflows gradually.\n  * Remove old parts after validating the replacement.\n\n\n\nIn front-end applications, this may include:\n\n  * Rebuilding one route.\n  * Moving one feature into a new module.\n  * Gradually replacing a central component.\n  * Introducing design tokens before replacing every component.\n  * Moving API requests into a unified layer feature by feature.\n\n\n\n#  The relationship between decisions, overengineering, code review, and technical debt\n\nThese topics are not separate.\n\nA poor technical decision may create overengineering.\n\nOverengineering may create technical debt.\n\nWeak code reviews may allow debt to accumulate.\n\nHeavy technical debt may make every new decision more difficult.\n\nThe opposite is also true:\n\n  * Conscious decisions reduce unnecessary complexity.\n  * Simple designs reduce maintenance cost.\n  * Good code reviews identify problems early.\n  * Technical debt management protects team velocity.\n\n\n\nThe role of a senior front-end developer is not limited to writing components.\n\nThe role includes protecting the team’s ability to continue developing the product.\n\n#  A complete practical scenario\n\nImagine that the team needs to add filters to a product listing page.\n\nThe current requirements are:\n\n  * Filter by price.\n  * Filter by category.\n  * Filter by rating.\n  * Share the filtered results through a link.\n  * Preserve filters after refreshing the page.\n\n\n\n##  A possible junior-level decision\n\nCreate a global store and place every filter inside it.\n\nThis may work, but it does not naturally support shareable URLs.\n\n##  A senior-level thought process\n\nFirst, classify the state.\n\nThe filters:\n\n  * Must be shareable through the URL.\n  * Must survive a browser refresh.\n  * Must affect the API query.\n  * Do not necessarily need to be global across the entire application.\n\n\n\nTherefore, URL search parameters may be the most appropriate source of truth.\n\nReact Query can then fetch product results based on the URL parameters.\n\nThis avoids:\n\n  * An unnecessary global store.\n  * Complex synchronization between the store and the URL.\n  * Losing filters after refresh.\n  * Manual share-link generation.\n\n\n\nNow imagine that a developer proposes building a generic filter engine that supports every possible filter type.\n\nThe team should ask:\n\n  * Do other pages need the same system?\n  * Are the future filter types known?\n  * Will the backend send dynamic filter definitions?\n  * Is building an engine now cheaper than implementing the current page directly?\n\n\n\nThe team may choose to implement the current filters clearly, then extract shared behavior when a second or third page appears.\n\nDuring code review, the reviewer notices that every input change immediately sends a request.\n\nThe reviewer asks:\n\n> Do we need debouncing for text filters? What happens if an older request finishes after a newer one?\n\nThe team may add `AbortController` or use a data-fetching library that manages request cancellation.\n\nAfter launch, the team notices that adding one new filter requires changes in five files.\n\nThe team records technical debt:\n\n> Centralize filter definitions into one configuration to reduce repeated changes.\n\nWhen the next filtered page is introduced, the team repays that debt because the duplication is now real and the pattern is understood.\n\nThis is an example of architecture growing with the problem rather than attempting to predict everything from the beginning.\n\n#  Practical principles for senior front-end developers\n\n##  1. Do not search for the perfect solution\n\nSearch for the solution that fits the current stage.\n\n##  2. Every decision has a cost\n\nEven excellent technologies have disadvantages.\n\n##  3. Simplicity is not a lack of experience\n\nReaching a simple solution after deeply understanding the problem is a sign of maturity.\n\n##  4. Do not measure code quality by the number of patterns\n\nMeasure it by clarity and ease of change.\n\n##  5. Do not optimize without measurement\n\nIdentify the bottleneck before attempting to improve it.\n\n##  6. Do not build for an imaginary future\n\nBuild for the reasonably expected future and leave room for evolution.\n\n##  7. Code review is a conversation\n\nIts goal is to improve the system and the team, not prove superiority.\n\n##  8. Technical debt is not always a failure\n\nThe real failure is unmanaged and invisible debt.\n\n##  9. Refactoring is not a goal by itself\n\nIt should reduce change cost, risk, or complexity.\n\n##  10. Understanding the business is part of technical engineering\n\nA good technical decision cannot be made without understanding what the product is trying to achieve.\n\n#  Conclusion\n\nA senior front-end developer is not the person who writes the most complicated TypeScript types, uses the largest number of libraries, or converts every component into a generic reusable system.\n\nA senior front-end developer is the person who knows:\n\n  * When to use a powerful tool.\n  * When a simple solution is enough.\n  * When to create an abstraction.\n  * When some duplication is acceptable.\n  * When delivery speed matters most.\n  * When a shortcut creates unacceptable risk.\n  * When code needs refactoring.\n  * When rebuilding would waste time.\n  * How to discuss technical decisions without ego.\n  * How to communicate trade-offs to engineering and business stakeholders.\n  * How to protect system quality without stopping product development.\n\n\n\nTechnical maturity is not visible only in the code you write.\n\nIt is also visible in the complexity you deliberately choose not to add.\n\nIt is visible in the modern technology you choose not to use because it does not serve the project.\n\nIt is visible in the pull request you review with clarity and respect.\n\nIt is visible in the technical debt you consciously accept and later repay before it becomes a serious burden.\n\nIt is visible in your ability to balance quality, speed, simplicity, and flexibility.\n\nUltimately, the success of a front-end architecture is not measured by how impressive it looks in a diagram.\n\nIt is measured by whether the team can add features, fix defects, understand the system, and continue developing it with confidence.\n\nThat is the real difference between a developer who knows how to write code and a software engineer who understands why the code should be written that way.",
  "title": "How Does a Senior Front-End Developer Think?"
}