GRASP in simple words: 9 principles that help to allocate responsibility in code
Main chat
A chat for vibe coders: news, guides, live cases, marketplace, and finding executors.
When the project starts, the architecture is simple. There is an order form, a payment button and several database queries. You can ask the AI to write OrderService, add up the calculation of the amount, create an order, pay, send a letter and get a working result in one evening.
The problem comes later. To add a promo code, you have to change the payment. To connect the second payment service, you need to rewrite half of OrderService. To test the calculation of the amount, you have to raise the database and replace HTTP requests. One class knows too much, does too much, and changes for too many reasons.
SOLID is usually remembered at this point. But SOLID tells you what properties good code should have, rather than always telling you who to put the new responsibility on.
This is the question that GRASP answers.
In short, **GRASP is the nine principles for allocating responsibility between objects and modules. They help us decide:
- who should calculate the amount of the order;
- who should create a new object;
- where to receive a request from the user;
- how to connect external services;
- how not to turn a controller or service into a God Object;
- where abstraction is needed, and where it will only complicate the project.
GRASP is not only useful in classical object-oriented programming. The same questions arise in TypeScript, Python, Java, C#, PHP, frontend applications, Telegram bots, and server functions. These principles work especially well when developing with AI: the model quickly generates locally correct code, but without explicit boundaries, often folds all scenarios into one large service.
What's GRASP
GRASP stands for General Responsibility Assignment Software Patterns: General Responsibility Patterns in Software.
Craig Larman, author of Applying UML and Patterns. GRASP has nine principles:
- Information Expert - Information Expert.
- The Creator is the creator.
- Controller, controller.
- Low Coupling is a weak connection.
- High Cohesion – High Cohesion or Module Focus.
- Polymorphism is polymorphism.
- Pure Fabrication is pure fiction.
- Indirection is the middleman.
- Protected Variations: Protection against change.
The word “patterns” can be confusing here. GRASP does not offer ready-made designs like Factory Method or Observer. It is rather a set of questions that help make architectural decisions.
For example, instead of asking “What file should I put this function in?” GRASP suggests asking:
Which entity already has the data needed to perform this operation?
The answer often immediately shows the right place for logic.
End-to-end example: ordering service
Imagine an online store. The AI generated this service:
class CheckoutService {
async checkout(input: CheckoutInput) {
const products = await db.products.findMany({
where: { id: { in: input.productIds } },
});
const subtotal = products.reduce(
(sum, product) => sum + product.price,
0,
);
const discount = input.promoCode
? await db.promocodes.calculate(input.promoCode, subtotal)
: 0;
const total = subtotal - discount;
const payment = await fetch("https://payment.example/charge", {
method: "POST",
body: JSON.stringify({ amount: total }),
});
const order = await db.orders.create({
data: {
email: input.email,
total,
paymentId: (await payment.json()).id,
},
});
await emailClient.send({
to: input.email,
subject: "Заказ оформлен",
template: "order-created",
data: order,
});
return order;
}
}
The code can work. But one method at the same time:
- loads the goods;
- calculate the amount;
- applies a discount;
- knows the payment API protocol;
- keeps the order;
- generates a notification;
- manages the entire user script.
This is a typical result of a “do the whole order” request. AI optimizes the solution for the visible scenario, not for future changes. GRASP allows you to disassemble this lump of responsibility according to understandable rules.
1. Information Expert: Entrust the work to the person who owns the data
Information Expert, or "information expert", is the guiding principle of GRASP.
Responsibility should be assigned to an object that already has the data necessary to carry it out.
Who should calculate the total order amount? Not a controller or a repository. The order itself already contains positions, prices and quantity, so it is an information expert.
type OrderLine = {
productId: string;
unitPrice: number;
quantity: number;
};
class Order {
constructor(private readonly lines: OrderLine[]) {}
subtotal(): number {
return this.lines.reduce(
(sum, line) => sum + line.unitPrice * line.quantity,
0,
);
}
}
Now the calculation is next to the data. If an amount, tax, or rounding appears, you don’t need to look for the formula in the controller, SQL query, and letter template.
Practical question Information Expert:
Who already has most of the data for this operation?
But the principle cannot be applied mechanically. The user’s object knows the email, but that doesn’t mean it has to open an SMTP connection on its own. Data and technical integration are different types of responsibilities. In such cases, other GRASP principles will be required.
2.Creator: The object must be created by its natural owner
The *Creator principle helps you choose who should create a new object.
A B object is a good candidate for a A object if B:
- contains or aggregates
A; - actively using
A; - stores records of
A; - has the data necessary to initialize
A.
The order contains order positions, so it is logical to create them through the order itself, rather than throwing OrderLine constructors over controllers.
class Order {
private readonly lines: OrderLine[] = [];
addProduct(product: Product, quantity: number): void {
this.lines.push({
productId: product.id,
unitPrice: product.price,
quantity,
});
}
subtotal(): number {
return this.lines.reduce(
(sum, line) => sum + line.unitPrice * line.quantity,
0,
);
}
}
Creator does not mean that any complex object must be assembled through new within a domain model. If building requires a database, configuration, multiple dependencies, or complex validation, a separate factory might be better.
The main idea is simpler: the creation of an object should occur where there is already a context for correct initialization.
3. Controller: Separate login from business logic
Controller in GRASP is an entity that receives a system event and forwards the work to suitable participants.
For an HTTP application, such an event may be POST /checkout. For the Telegram bot, the /buy command. For the queue is the incoming OrderRequested message.
Good controller:
- receives and checks the form of the input data;
- calls the application script;
- turns the result into an HTTP response or message;
- does not calculate prices and does not implement payment rules.
class CheckoutController {
constructor(private readonly checkout: CheckoutUseCase) {}
async handle(request: CheckoutRequest): Promise<CheckoutResponse> {
const result = await this.checkout.execute({
customerEmail: request.body.email,
items: request.body.items,
paymentMethod: request.body.paymentMethod,
});
return {
status: 201,
body: result,
};
}
}
The controller coordinates but does not become the owner of the business rules.
A common mistake is to create a “thin” HTTP controller that calls a huge CheckoutService. Formally, the controller will remain small, but God Object will simply move to another file. GRASP should be applied to the entire chain, not just the transport layer.
4. Low Coupling: Reduce Your Neighborhood Knowledge
**Low Coupling, or weak connectivity, means that a module should know as little as possible about the details of other modules.
In the original example, CheckoutService knows the URL of the payment service, the JSON format, the authorization method and the response structure. Any change to the external API will affect the business scenario.
Instead, the scenario may depend on a small contract:
type ChargeResult = {
transactionId: string;
};
interface PaymentGateway {
charge(amount: number): Promise<ChargeResult>;
}
class CheckoutUseCase {
constructor(private readonly paymentGateway: PaymentGateway) {}
}
Specific integration remains at the edge of the system:
class AcmePaymentGateway implements PaymentGateway {
async charge(amount: number): Promise<ChargeResult> {
const response = await fetch("https://payment.example/charge", {
method: "POST",
body: JSON.stringify({ amount }),
});
const payload = await response.json();
return { transactionId: payload.id };
}
}
Weak connectivity offers practical benefits:
- the payment provider can be replaced locally;
- the scenario is easy to test without a network;
- external API details are not spread over the project;
- changes are becoming more predictable.
But minimal connectivity is not an end in itself. If seven interfaces and four adapters appear for a single call, the cost of abstraction may be higher than the benefit. It’s important to isolate real change points, not every line of code.
5. High Cohesion: One module must solve a focused task
The term “High Cohesion” is often translated as “high connectivity.” It is easily confused with the connection between modules. In the sense we are talking about internal integrity: the duties of one object should be closely related.
CheckoutService has low cohesion because pricing, HTTP integration, saving and sending emails change for different reasons.
After separation, the roles may look like this:
CheckoutController -> accepts HTTP request
CheckoutUseCase -> coordinates the script
Order -> stores the order rules and calculates the amount
DiscountPolicy -> calculates the discount
PaymentGateway -> PaymentGateway
OrderRepository -> Keep order
OrderNotifier -> sends a notification
Each element has a clear reason to change.
To check the connection can be a simple question:
Is it possible to describe the purpose of this class in one sentence without the words “and also?”?
If the description sounds like “places an order, sends emails, writes an audit, and syncs CRM,” the boundaries are almost certainly poorly chosen.
6. Polymorphism: Replace Branches with Interchangeable Implementations
*Polymorphism suggests entrusting different behaviors to the types themselves rather than assembling growing if or switch in one place.
Bad sign:
if (method == "card") {
// one protocol
} other if (method == "sbp") {
// other protocol
} other if (method == "invoice" {
// third protocol
}
Each new payment method changes the overall scenario. Instead, interchangeable handlers can be used:
interface PaymentMethod {
pay(amount: number): Promise<ChargeResult>;
}
class CardPayment implements PaymentMethod {
async pay(amount: number): Promise<ChargeResult> {
// Оплата картой
return { transactionId: "card-transaction" };
}
}
class SbpPayment implements PaymentMethod {
async pay(amount: number): Promise<ChargeResult> {
// Оплата через СБП
return { transactionId: "sbp-transaction" };
}
}
In TypeScript, polymorphism does not require a complex class hierarchy. Often, an interface and several ordinary objects or functions with the same contract are sufficient.
Applying the principle is worthwhile when behaviors do develop independently. If the condition is one, stable, and never expands, a separate hierarchy will only make reading difficult.
7. Pure Fabrication: Sometimes you have to come up with a useful object
Not every technical obligation has a natural object in the domain. Who's responsible for keeping the order? Order itself does not need to know SQL and table structure. The buyer doesn't fit either.
**Pure Fabrication, or "pure fiction," allows you to create a technical object that is not in real business, if it increases cohesion and reduces coupling.
Typical examples:
OrderRepository;EmailNotifier;PaymentGateway;UserMapper;AuditLogger.
interface OrderRepository {
save(order: Order): Promise<void>;
}
class PostgresOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
// Преобразование модели и запись в PostgreSQL
}
}
The repository is not an object of the online store business. It was invented by the developer so that the domain model does not depend on PostgreSQL.
The danger of Pure Fabrication is the uncontrolled reproduction of services with vague names: CommonService, HelperManager, UtilsProvider. A fictional object is only useful if it has a clear technical responsibility.
8. Indirection: Add a mediator between things that don’t have to depend on you
Indirection suggests entering an intermediate object so that the two components do not know each other directly.
Example: After creating an order, you need to send a letter, record the event in analytics and transfer the data to CRM. If CheckoutUseCase directly calls all three systems, it will quickly become dependent.
The mediator can be the event manager:
type OrderCreated = {
orderId: string;
customerEmail: string;
};
interface EventBus {
publish(event: OrderCreated): Promise<void>;
}
class CheckoutUseCase {
constructor(private readonly events: EventBus) {}
async execute(input: CheckoutInput): Promise<void> {
// Создание и сохранение заказа
await this.events.publish({
orderId: "order-id",
customerEmail: input.customerEmail,
});
}
}
Subscribers can independently send an email, update analytics, and sync CRM.
The intermediary reduces direct connectivity, but adds a new execution route. The event scheme complicates debugging, error handling and delivery guarantees. For a small application, the usual OrderNotifier call may be clearer.
Indirection isn't everywhere. Its price is justified when a direct dependency does prevent change or reuse.
9. Protected Variations: Put a stable border around an unstable part
*Protected Variations is the principle of protection against change. First, you need to find a part of the system that is likely to change, and then surround it with a stable contract.
The most common changes are:
- payment and logistics APIs;
- e-mail and SMS providers;
- structure of external webhooks;
- aI models and providers;
- databases and cloud storage;
- discounts and rates.
If a project accesses OpenAI, Anthropic, or a local model directly from dozens of files, replacing the provider will be expensive. A stable boundary localizes the change:
interface TextGenerator {
generate(prompt: string): Promise<string>;
}
class OpenAiTextGenerator implements TextGenerator {
async generate(prompt: string): Promise<string> {
// Детали OpenAI API находятся только здесь
return "generated text";
}
}
This is especially important for AI integration. The provider may change the model, tool call format, limits, error pattern or retroactive policy. Business logic doesn’t have to know every detail.
Protected Variations is similar to Low Coupling, but focuses on the predicted point of instability. You don't have to abstract everything. Protect what is changing or out of your control.
How nine principles fit into one architecture
After applying GRASP, ordering can look like this:
HTTP request
|
CheckoutConroller Controller
|
CheckoutUseCase High Cohesion
Order Information Expert + Creator
|-- DiscountPolicy Polymorphism
|-- PaymentGateway Protected Variations
OrderRepository Pure Fabrication
EventBus Indirection
All dependencies between parts try to keep weak: Low Coupling.
GRASP does not give the only correct scheme. It helps to argue the decision:
- the amount is in
Orderbecause the order holds the positions; - The HTTP controller only accepts a system event;
- the payment API is hidden behind a stable contract;
- the repository is created as a technical abstraction;
- different payment methods implement common behavior;
- external reactions can be separated through an intermediary.
So architecture ceases to be a set of taste preferences. There is a verifiable reason for each decision.
GRASP and SOLID: What is the difference
GRASP and SOLID do not compete. They work at different levels and complement each other.
| GRASP | SOLID |
|---|---|
| Помогает назначать ответственность | Помогает оценивать качество границ и зависимостей |
| Спрашивает: «кто должен это делать?» | Спрашивает: «насколько легко это менять и расширять?» |
| Ориентирован на проектирование взаимодействий | Ориентирован на устойчивость модулей и контрактов |
| Содержит Information Expert, Creator, Controller | Содержит SRP, OCP, LSP, ISP, DIP |
| Часто применяется при первом разбиении сценария | Часто применяется при проверке и рефакторинге решения |
The practical order may be:
- Using GRASP to determine who owns each responsibility.
- Using SOLID to check whether too wide roles and fragile dependencies.
- Using чистой архитектуры, determine the direction of dependencies between large layers.
A detailed analysis of the five principles is already available in the «SOLID простыми словами» article.
How to apply GRASP to code written by an AI
AI rarely disrupts architecture with one obvious mistake. More often, it gradually expands the existing service: adds another dependency, another if, another database request and another technical obligation.
Therefore, the query “GRASP refactors” is too vague. It is better to get the agent to explain the allocation of responsibility first.
Working prompt:
Analyze this scenario before changing the code.
1. List all responsibilities of the current modules.
2. For each liability, specify an Information Expert.
3. Identify a Controller object for a system event.
4. Find modules with low cohesion and too strong coupling.
5. Find branches that are better replaced by polymorphism.
6. Note unstable external integrations for Protected Variations.
7. Offer a minimal change in structure without unnecessary abstractions.
8. Only after agreeing on the plan, change the code and add tests.
This request is more useful than “make architecture beautiful.” It requires the model to show causality and allows you to notice overcomplication before generating dozens of files.
GRASP can also be added to the agent’s design instructions:
Before adding a new business logic, identify the owner of the liability:
Place data and calculations with the Information Expert;
Leave the transport controllers thin;
Isolate volatile external APIs with stable contracts
Do not add an interface or intermediary without a specific change point.
Explicitly report if the existing module loses cohesion.
When GRASP becomes overcomplicated
GRASP is not a requirement to create a separate class for each function.
For a small script, a one-time migration, or a simple CRUD script, direct code may be the best solution. Architectural design is justified when it reduces the cost of expected changes.
Disturbing signs of inflection:
- the interface has only one implementation and no change points are visible;
- a simple action takes place through five intermediaries;
- classes consist of one method and do not possess a state or rules;
- names like
Manager,Processor,Handlerhide unclear liability; - to understand the scenario, you have to open ten files;
- the tests test moles, not business behavior.
GRASP should be used as a set of heuristics, not as a layer generator. A good solution remains simple enough for a new developer to go from entry to result.
GRASP practical checklist for review
Before adding a new feature or during a review, ask nine questions:
- Information Expert: Which entity already has data for this operation?
- Creator: Who naturally contains, uses or initializes the object being created?
- Controller: Who receives the system event and where does the transport logic end?
- **Low Coupling: ** How many other details does this module know?
- **High Cohesion: ** Can you describe his role in one sentence without a “but”?
- Polymorphism: Does
switchgrow with each new variant? - Pure Fabrication: Do you need a separate technical entity to store, ship or convert?
- Indirection: Does direct dependence interfere with modification or testing of components?
- Protected Variations: Which part is most unstable and where to put a stable border?
It is not necessary to apply all nine principles to each task. The value of the checklist is that architectural decisions become conscious, not accidental.
Frequent questions
What is GRASP in simple words?
GRASPs are nine principles that help decide which object or module should be responsible for a particular action. They reduce connectivity, keep classes focused, and protect the project from change.
How many principles are included in GRASP?
The classic set includes nine principles: Information Expert, Creator, Controller, Low Coupling, High Cohesion, Polymorphism, Pure Fabrication, Indirection and Protected Variations.
How is GRASP different from SOLID?
GRASP helps to allocate responsibilities between objects, and SOLID helps to check the stability of the resulting modules and dependencies. GRASP often answers the question "Who should do this?", SOLID - "How to make it variable and extensible?".
Can GRASP be used in functional programming?
Yeah. Although GRASP terminology has emerged in object-oriented design, ideas of information expert, weak connectivity, high cohesion, intermediaries, and change protection are applicable to functions, modules, and services.
Do you need GRASP for a small project?
For a small project, it is enough to use GRASP as a checklist. There is no need to build a complex architecture in advance. First, identify the owners of business rules and isolate the really volatile externalities.
Why is GRASP useful when developing with AI?
AI performs individual functions well, but often places new responsibilities in the nearest existing service. GRASP provides specific questions to verify the outcome and helps prevent God Object from appearing as the project grows.
Main conclusion
Most architectural problems don’t start with a bad syntax or a wrong framework. They start when the new responsibility falls to the wrong owner.
GRASP offers a practical way to make these decisions:
- data prompts the information expert;
- the natural owner creates nested objects;
- the controller accepts the event but does not assign business logic;
- external and variable parts have stable boundaries;
- the modules remain focused and know only what is necessary about each other.
You do not need to implement all nine patterns at the same time. Start with three questions: who owns the data, who hosts the system event, and what part of the system will change most often. That’s enough to make code written by humans or AI grow more predictable.
Craig Larman, Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development.