Skip to main content
RapidDev - Software Development Agency
cursor-tutorial

How to prevent bad architecture from Cursor

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.

What you'll learn

  • Why Cursor defaults to global variables and how to prevent it
  • How to configure .cursorrules for modular architecture
  • How to refactor existing globals into proper modules with Cursor
  • How to detect global state patterns in your codebase
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner7 min read10-15 minCursor Free+, any languageMarch 2026RapidDev Engineering Team
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.

Prerequisites

  • 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.

.cursor/rules/architecture.mdc
1---
2description: Architecture rules - no global state
3globs: "src/**/*.ts,src/**/*.tsx"
4alwaysApply: true
5---
6
7## Global State Rules
8- NEVER use global variables or module-level mutable state
9- NEVER use the singleton pattern with module-level instances
10- NEVER store state in file-scoped 'let' variables
11- ALWAYS pass dependencies through function parameters or constructors
12- ALWAYS use React Context or Zustand for shared UI state
13- For configuration: use an imported config module (readonly)
14- For caching: use a dedicated cache service passed via DI
15- For logging: accept a logger parameter, do not import a global logger
16
17## Allowed Module-Level Declarations
18- const with primitive values or frozen objects
19- Type definitions and interfaces
20- Pure functions with no side effects
21- 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
1// Cursor Chat prompt (Cmd+L, Ask mode):
2// @codebase Find all instances of global state in src/:
3// 1. Module-level 'let' or 'var' declarations
4// 2. Singleton patterns (getInstance())
5// 3. Mutable objects assigned at module scope
6// 4. Global event emitters or bus instances
7// 5. window.* or globalThis.* assignments
8// 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.

src/db/pool.ts
1// BEFORE (global singleton):
2let instance: DatabasePool | null = null;
3export function getPool(): DatabasePool {
4 if (!instance) {
5 instance = new DatabasePool(process.env.DATABASE_URL);
6 }
7 return instance;
8}
9
10// Select the code, press Cmd+K:
11// Refactor this global singleton into a factory function
12// that creates a pool instance. The instance should be
13// created once in the app entry point and passed to
14// services via constructor injection.
15
16// AFTER:
17export function createPool(url: string): DatabasePool {
18 return new DatabasePool(url);
19}
20
21// In app entry point:
22const pool = createPool(config.databaseUrl);
23const 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.

src/services/orderService.ts
1// BEFORE:
2import { logger } from './globalLogger';
3export function processOrder(order: Order) {
4 logger.info('Processing order', { orderId: order.id });
5}
6
7// Cmd+K prompt:
8// Refactor to accept a logger parameter instead of
9// importing a global logger. Use an ILogger interface.
10
11// AFTER:
12import type { ILogger } from '@/types/interfaces';
13export function processOrder(order: Order, logger: ILogger) {
14 logger.info('Processing order', { orderId: order.id });
15}

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.

src/services/orderService.test.ts
1// Cursor Chat prompt (Cmd+L):
2// @src/services/orderService.ts Generate a test that:
3// 1. Creates a mock logger
4// 2. Calls processOrder with the mock
5// 3. Verifies the mock logger was called correctly
6// 4. Proves no global state is accessed
7
8import { processOrder } from './orderService';
9import type { ILogger } from '@/types/interfaces';
10
11const mockLogger: ILogger = {
12 info: vi.fn(),
13 error: vi.fn(),
14 warn: vi.fn(),
15};
16
17it('uses the injected logger', () => {
18 processOrder({ id: '123' } as Order, mockLogger);
19 expect(mockLogger.info).toHaveBeenCalledWith(
20 'Processing order', { orderId: '123' }
21 );
22});

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

Complete working example

.cursor/rules/architecture.mdc
1---
2description: Modular architecture rules - no global state
3globs: "src/**/*.{ts,tsx,js,jsx}"
4alwaysApply: true
5---
6
7## Forbidden Patterns
8- `let` or `var` at module/file scope (mutable globals)
9- Singleton pattern with `getInstance()` or module-level instances
10- `window.*` or `globalThis.*` assignments
11- Global event buses without explicit subscription management
12- Module-level `Map`, `Set`, or `Array` used as shared state
13
14## Required Patterns
15- Pass dependencies through constructor or function parameters
16- Create instances in the application entry point (composition root)
17- Use factory functions instead of singletons
18- Use React Context or state management libraries for UI state
19- Use readonly config objects (created once, never mutated)
20
21## Module-Level Allowed
22- `const` with immutable primitive values
23- `const` with Object.freeze() for config objects
24- Type/interface/enum definitions
25- Pure function declarations
26- Re-exports from barrel index files
27
28## How to Share State
29| Need | Solution |
30|------|----------|
31| Database pool | Factory function, inject via constructor |
32| Logger | Accept ILogger parameter |
33| Configuration | Readonly config module |
34| Cache | Cache service, inject via constructor |
35| UI state | React Context or Zustand store |
36| Event handling | Typed EventEmitter, inject via constructor |

Common mistakes when preventing bad architecture from Cursor

Why it's a problem: Cursor creating module-level cache Maps

How to avoid: Add 'NEVER create module-level Map, Set, or Array for state' to .cursorrules. Use a cache service passed via DI.

Why it's a problem: Cursor importing a global logger directly

How to avoid: Add 'Accept a logger parameter, do not import a global logger' to your rules.

Why it's a problem: Cursor generating singleton getInstance() patterns

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Create architecture rules for a TypeScript project that forbid global variables, singletons, module-level mutable state, and global event buses. Provide allowed alternatives for each: factory functions for singletons, constructor injection for dependencies, readonly config for settings, and React Context for UI state. Include a comparison table.

Cursor Prompt

In Cursor Chat (Cmd+L): @codebase @.cursor/rules/architecture.mdc Find all global state patterns in src/. For each global variable, singleton, or module-level mutable state, suggest a refactoring to use dependency injection or factory functions instead.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.