Skip to the main content.

3 min read

Magento 2 Code Architecture Best Practices

Magento 2 Code Architecture Best Practices
Magento 2 Code Architecture Best Practices
6:10

Table of Contents:

Using the Framework Layer Appropriately

Magento’s Framework layer (Magento\Framework\*) is the backbone of application services: it powers dependency injection, routing, caching, and more. But it’s not where business logic belongs.

Use the Framework layer to:

  • Access infrastructure-level services: Such as file I/O, date handling, request/response objects, or cache pools.
  • Integrate low-level utilities: Like SearchCriteriaBuilder or Filesystem that support modular service behavior.
  • Extend generic behaviors: Like data serialization or HTTP cookie management.

What to avoid:

  • Injecting MessageManagerInterface or Session objects into service classes, which tightly couples logic to the UI layer.
  • Using framework classes inside domain logic without creating a service abstraction.

Why it matters: By depending directly on framework classes in business logic, you limit the portability of that code. It becomes harder to reuse in APIs, CLI tools, or background jobs, and test coverage suffers as a result.

Designing Modules with Isolation in Mind

Each Magento module should represent a focused, testable unit of behavior—not a grab bag of unrelated logic. When modules are scoped too broadly or entangled with each other, refactoring becomes risky and slow.

In a well-structured module:

  • Interfaces live in the Api/ directory, defining what the module exposes.
  • Data transfer objects (DTOs) live in Api/Data/, enabling type-safe communication.
  • Services are separated from delivery mechanisms (controllers, observers, plugins).
  • Configuration and wiring are declared in di.xml, module.xml, and etc/config.xml, and reflect only what’s actually used.

Poorly designed modules often:

  • Contain both frontend and admin logic in a single module.
  • Include controller logic that performs business operations directly instead of calling services.
  • Register unnecessary observers or plugins that lead to side effects across the system.

Tip for maintainability: Keep modules loosely coupled and explicitly versioned. Use sequence in module.xml to manage load order and ensure each module is deployable and testable on its own.

Dependency Injection: How to Do It Right

Magento’s DI container enables clean object creation, but misuse is common. Injecting too many services, relying on concrete classes, or bypassing DI entirely with ObjectManager::get() can quickly turn a flexible system into a fragile one.

Use DI for:

  • Required services used on every code path.
  • Interfaces instead of concrete implementations—making it easier to mock or replace.
  • Testable, predictable constructor logic free from side effects or conditional behavior.

Avoid DI when:

  • Injecting expensive dependencies like full product collections that may not always be used.
  • Loading optional behavior that should be lazy-loaded or determined at runtime.
  • You’re tempted to inject ObjectManager to resolve dynamic dependencies (use factories or proxies instead).

Patterns that help:

  • Proxy classes are ideal for expensive dependencies like ProductRepository or StockRegistry when access is conditional.
  • Factory classes allow instantiation of objects that require dynamic data at runtime.
  • Custom service factories can encapsulate construction logic and keep constructors lightweight.

Watch out: If a class has more than 5–7 dependencies, it's a strong sign it needs to be broken down. Class responsibilities should be narrow and clearly defined.

Interface Contracts and Testing Discipline

Good Magento code separates interface from implementation—and that separation is what enables strong, maintainable tests.

Use the Api/ directory for service contracts and the @api annotation only when those interfaces are intended for public consumption (e.g., reusable by other modules or integrations). Avoid exposing internal logic unnecessarily.

Testing tips:

  • Unit tests should cover service classes by mocking injected dependencies.
  • Integration tests should verify service behavior against real database and configuration contexts.
  • Keep logic out of controllers—move it to services, and test those services in isolation.
  • Prefer ViewModels over helpers for data formatting in templates—they’re easier to test and maintain.

Practical example: Rather than formatting a price directly in a .phtml file or helper, inject a ViewModel that formats based on store context and uses PriceCurrencyInterface to remain locale-aware.

File Structure and Readability

Magento’s file structure is deep—but clean organization pays off in the long run.

Follow PSR-4 naming conventions:

  • Classes in Model/, Service/, ViewModel/, Observer/, and Plugin/ should reflect clear intent.

Group logic by responsibility, not by delivery method. For example, avoid putting data fetch logic in a controller—it belongs in a service or repository.

Avoid:

  • Massive helper classes doing too many unrelated things.
  • Using raw SQL in business logic—always go through ResourceModel and Collection patterns.
  • Duplicating logic across observers, controllers, and cron jobs. Consolidate it in services and call those services from each context.

Maintain readable, self-documenting code. Favor expressive class names over comments explaining behavior. And when in doubt, add a README to each module to clarify its purpose and key dependencies.

Final Thoughts

Magento 2 gives developers a powerful architecture—but it doesn’t enforce good practices out of the box. That responsibility lies with your team.

By adhering to clear boundaries, isolating business logic, and respecting DI and modularity principles, you gain more than just cleaner code—you get a codebase that enables rapid change, safe extension, and easier onboarding.

Your future developers—and your business stakeholders—will thank you for writing Magento code that scales.

Read more related blogs