# How to prevent bad architecture from Cursor

- Tool: Cursor
- Difficulty: Beginner
- Time required: 10-15 min
- Compatibility: Cursor Free+, any language
- Last updated: March 2026

## TL;DR

Cursor frequently introduces global variables, singletons, and module-level mutable state that break modular design and make testing difficult. By adding architecture rules to .cursorrules that forbid global state and mandate dependency injection, you prevent Cursor from generating tightly coupled code that becomes unmaintainable as your project grows.

## Preventing bad architecture from Cursor

Global variables are the fastest way to share state, which is why Cursor reaches for them. But they create hidden dependencies, make tests unpredictable, and cause bugs when multiple parts of the code modify the same state. This tutorial configures Cursor to use proper module patterns instead.

## Before you start

- Cursor installed with a project open
- Code that may contain global state patterns
- Understanding of module exports and imports
- Familiarity with Cursor Chat (Cmd+L) and Cmd+K

## Step-by-step guide

### 1. Add anti-global rules to .cursor/rules

Create rules that explicitly forbid global state patterns and redirect Cursor toward modular alternatives.

```
---
description: Architecture rules - no global state
globs: "src/**/*.ts,src/**/*.tsx"
alwaysApply: true
---

## Global State Rules
- NEVER use global variables or module-level mutable state
- NEVER use the singleton pattern with module-level instances
- NEVER store state in file-scoped 'let' variables
- ALWAYS pass dependencies through function parameters or constructors
- ALWAYS use React Context or Zustand for shared UI state
- For configuration: use an imported config module (readonly)
- For caching: use a dedicated cache service passed via DI
- For logging: accept a logger parameter, do not import a global logger

## Allowed Module-Level Declarations
- const with primitive values or frozen objects
- Type definitions and interfaces
- Pure functions with no side effects
- Readonly configuration objects
```

**Expected result:** Cursor avoids global state and uses dependency injection or module parameters instead.

### 2. Audit existing code for global state

Use Cursor to find all global state patterns in your codebase. This identifies the scope of the problem before you start fixing.

```
// Cursor Chat prompt (Cmd+L, Ask mode):
// @codebase Find all instances of global state in src/:
// 1. Module-level 'let' or 'var' declarations
// 2. Singleton patterns (getInstance())
// 3. Mutable objects assigned at module scope
// 4. Global event emitters or bus instances
// 5. window.* or globalThis.* assignments
// List each file, the pattern found, and suggest a modular alternative.
```

**Expected result:** A comprehensive list of global state patterns with suggested refactoring approaches.

### 3. Refactor a global singleton to dependency injection

Select a singleton pattern and use Cmd+K to refactor it into a proper dependency-injected class. Reference your architecture rules so Cursor follows the modular pattern.

```
// BEFORE (global singleton):
let instance: DatabasePool | null = null;
export function getPool(): DatabasePool {
  if (!instance) {
    instance = new DatabasePool(process.env.DATABASE_URL);
  }
  return instance;
}

// Select the code, press Cmd+K:
// Refactor this global singleton into a factory function
// that creates a pool instance. The instance should be
// created once in the app entry point and passed to
// services via constructor injection.

// AFTER:
export function createPool(url: string): DatabasePool {
  return new DatabasePool(url);
}

// In app entry point:
const pool = createPool(config.databaseUrl);
const userService = new UserService(pool);
```

**Expected result:** Singleton refactored to a factory function with explicit dependency passing.

### 4. Replace global logger with injected logger

Global loggers are the most common anti-pattern. Refactor them to accept a logger parameter so each module can have its own logger context.

```
// BEFORE:
import { logger } from './globalLogger';
export function processOrder(order: Order) {
  logger.info('Processing order', { orderId: order.id });
}

// Cmd+K prompt:
// Refactor to accept a logger parameter instead of
// importing a global logger. Use an ILogger interface.

// AFTER:
import type { ILogger } from '@/types/interfaces';
export function processOrder(order: Order, logger: ILogger) {
  logger.info('Processing order', { orderId: order.id });
}
```

**Expected result:** Logger passed as a parameter, making the function testable with a mock logger.

### 5. Verify the refactoring with tests

Generate tests that prove the refactored code works without global state. The key test is that each function can be called with different dependencies.

```
// Cursor Chat prompt (Cmd+L):
// @src/services/orderService.ts Generate a test that:
// 1. Creates a mock logger
// 2. Calls processOrder with the mock
// 3. Verifies the mock logger was called correctly
// 4. Proves no global state is accessed

import { processOrder } from './orderService';
import type { ILogger } from '@/types/interfaces';

const mockLogger: ILogger = {
  info: vi.fn(),
  error: vi.fn(),
  warn: vi.fn(),
};

it('uses the injected logger', () => {
  processOrder({ id: '123' } as Order, mockLogger);
  expect(mockLogger.info).toHaveBeenCalledWith(
    'Processing order', { orderId: '123' }
  );
});
```

**Expected result:** Tests pass using only injected dependencies, proving no global state is needed.

## Complete code example

File: `.cursor/rules/architecture.mdc`

```markdown
---
description: Modular architecture rules - no global state
globs: "src/**/*.{ts,tsx,js,jsx}"
alwaysApply: true
---

## Forbidden Patterns
- `let` or `var` at module/file scope (mutable globals)
- Singleton pattern with `getInstance()` or module-level instances
- `window.*` or `globalThis.*` assignments
- Global event buses without explicit subscription management
- Module-level `Map`, `Set`, or `Array` used as shared state

## Required Patterns
- Pass dependencies through constructor or function parameters
- Create instances in the application entry point (composition root)
- Use factory functions instead of singletons
- Use React Context or state management libraries for UI state
- Use readonly config objects (created once, never mutated)

## Module-Level Allowed
- `const` with immutable primitive values
- `const` with Object.freeze() for config objects
- Type/interface/enum definitions
- Pure function declarations
- Re-exports from barrel index files

## How to Share State
| Need | Solution |
|------|----------|
| Database pool | Factory function, inject via constructor |
| Logger | Accept ILogger parameter |
| Configuration | Readonly config module |
| Cache | Cache service, inject via constructor |
| UI state | React Context or Zustand store |
| Event handling | Typed EventEmitter, inject via constructor |
```

## Common mistakes

- **Cursor creating module-level cache Maps** — Cursor often adds `const cache = new Map()` at file scope for memoization. This creates hidden shared state that leaks between tests. Fix: Add 'NEVER create module-level Map, Set, or Array for state' to .cursorrules. Use a cache service passed via DI.
- **Cursor importing a global logger directly** — Global logger imports are the most common pattern in tutorials, so Cursor replicates it frequently. Fix: Add 'Accept a logger parameter, do not import a global logger' to your rules.
- **Cursor generating singleton getInstance() patterns** — Singletons are a well-known pattern in Cursor's training data, but they create untestable global state. Fix: Forbid singletons in .cursorrules. Use factory functions and create instances at the composition root.

## Best practices

- Forbid global mutable state explicitly in .cursor/rules
- Pass all dependencies through function parameters or constructors
- Create all instances at the application entry point (composition root)
- Use factory functions instead of singleton patterns
- Accept ILogger interfaces as parameters instead of importing global loggers
- Use Object.freeze() for configuration objects at module scope
- Test every function with injected dependencies to verify no global state leaks

## Frequently asked questions

### Are all module-level variables bad?

No. Immutable constants (const MAX_RETRIES = 3), frozen config objects, type definitions, and pure functions are fine at module level. Only mutable state (let, unfrozen objects used as caches) is problematic.

### How do I share a database connection pool without a global?

Create the pool in your app entry point and pass it to services through their constructors. This is the composition root pattern.

### Is React Context considered global state?

React Context is scoped to the provider tree, not truly global. It is the recommended way to share state in React. Just avoid putting everything in a single context.

### What about environment variables (process.env)?

Access process.env in a single config module and export a readonly object. All other modules import from the config module, never from process.env directly.

### Can I use Zustand stores without them being global?

Zustand stores are technically module-level singletons, but they are designed for this purpose with proper subscription management. They are an acceptable exception to the no-singleton rule for UI state.

---

Source: https://www.rapidevelopers.com/cursor-tutorial/how-to-keep-cursor-ai-from-recommending-global-variables-that-break-modular-design
© RapidDev — https://www.rapidevelopers.com/cursor-tutorial/how-to-keep-cursor-ai-from-recommending-global-variables-that-break-modular-design
