Skip to content

Ember Services — Shared State and Application Logic

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Ember Services. We cover key concepts, practical examples, and best practices to help you master this topic.

Ember services are Singleton objects that persist across route changes and component lifecycles. They manage shared state like user sessions, shopping carts, notification queues, and API clients. Services are injected into routes, components, and other services through Ember's Dependency Injection system.

What You'll Learn

You will learn how to create services, inject them into components and routes, manage shared state with tracked properties, and use services for cross-component communication.

Why It Matters

Without services, passing data between unrelated components requires prop drilling or event buses. Services provide a clean, testable way to share state across the entire application.

Real-World Use

A session service manages authentication state. When the user logs in, the session service updates. Every component that needs auth state injects the session service and reads isAuthenticated. The service also provides login, logout, and token refresh methods.

flowchart TD
    A[Session Service] --> B[Route A]
    A --> C[Route B]
    A --> D[Component X]
    A --> E[Component Y]
    F[Shopping Cart Service] --> D
    F --> G[Route C]
    H[Notification Service] --> B
    H --> C
    H --> D

Creating a Service

ember generate service session
// app/services/session.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class SessionService extends Service {
  @tracked user = null;
  @tracked accessToken = null;
  @tracked isAuthenticated = false;

  @action
  async login(email, password) {
    try {
      let response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
        headers: { 'Content-Type': 'application/json' }
      });

      let data = await response.json();
      this.user = data.user;
      this.accessToken = data.token;
      this.isAuthenticated = true;

      return { success: true };
    } catch (error) {
      console.error('Login failed:', error);
      return { success: false, error };
    }
  }

  @action
  logout() {
    this.user = null;
    this.accessToken = null;
    this.isAuthenticated = false;
  }

  get isAdmin() {
    return this.user?.role === 'admin';
  }
}

Injecting Services

Use the @service decorator to inject services into any Ember object.

// app/components/nav-bar.js
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';

export default class NavBarComponent extends Component {
  @service session;
  @service router;
  @service notifications;

  get userInitials() {
    return this.session.user?.name
      ?.split(' ')
      ?.map(n => n[0])
      ?.join('')
      ?.toUpperCase();
  }

  @action
  async handleLogin(email, password) {
    let result = await this.session.login(email, password);
    if (result.success) {
      this.notifications.success('Welcome back!');
      this.router.transitionTo('dashboard');
    } else {
      this.notifications.error('Login failed. Check your credentials.');
    }
  }

  @action
  handleLogout() {
    this.session.logout();
    this.router.transitionTo('index');
  }
}

Shopping Cart Service Example

// app/services/cart.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class CartService extends Service {
  @tracked items = [];
  @tracked discountCode = null;

  get itemCount() {
    return this.items.reduce((sum, item) => sum + item.quantity, 0);
  }

  get subtotal() {
    return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }

  get discount() {
    if (!this.discountCode) return 0;
    // Apply 10% discount
    return this.subtotal * 0.1;
  }

  get total() {
    return this.subtotal - this.discount;
  }

  @action
  addItem(product, quantity = 1) {
    let existing = this.items.find(item => item.id === product.id);
    if (existing) {
      existing.quantity += quantity;
    } else {
      this.items = [...this.items, {
        id: product.id,
        name: product.name,
        price: product.price,
        quantity
      }];
    }
  }

  @action
  removeItem(productId) {
    this.items = this.items.filter(item => item.id !== productId);
  }

  @action
  updateQuantity(productId, quantity) {
    let item = this.items.find(item => item.id === productId);
    if (item) {
      item.quantity = quantity;
    }
  }

  @action
  applyDiscount(code) {
    this.discountCode = code;
  }

  @action
  clear() {
    this.items = [];
    this.discountCode = null;
  }
}

Notification Service

// app/services/notifications.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class NotificationService extends Service {
  @tracked notifications = [];

  @action
  success(message, duration = 3000) {
    this.add(message, 'success', duration);
  }

  @action
  error(message, duration = 5000) {
    this.add(message, 'error', duration);
  }

  @action
  warning(message, duration = 4000) {
    this.add(message, 'warning', duration);
  }

  @action
  info(message, duration = 3000) {
    this.add(message, 'info', duration);
  }

  add(message, type, duration) {
    let id = Date.now();
    this.notifications = [...this.notifications, { id, message, type }];

    setTimeout(() => {
      this.dismiss(id);
    }, duration);
  }

  @action
  dismiss(id) {
    this.notifications = this.notifications.filter(n => n.id !== id);
  }
}
{{! app/components/notification-container.hbs }}
<div class="notification-container {{if this.notifications.notifications.length 'has-notifications'}}">
  {{#each this.notifications.notifications as |notification|}}
    <div class="notification notification-{{notification.type}}"
         role="alert"
         {{on "click" (fn this.notifications.dismiss notification.id)}}>
      {{notification.message}}
    </div>
  {{/each}}
</div>

Service Lifecycle and Initialization

// app/services/config.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';

export default class ConfigService extends Service {
  @tracked theme = 'light';
  @tracked language = 'en';
  @tracked features = {};

  constructor() {
    super(...arguments);
    this.loadConfig();
  }

  async loadConfig() {
    try {
      let response = await fetch('/api/config');
      let config = await response.json();
      this.theme = config.theme || 'light';
      this.language = config.language || 'en';
      this.features = config.features || {};
    } catch (error) {
      console.warn('Could not load config, using defaults');
    }
  }
}

Testing Services

// tests/unit/services/cart-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Service | cart', function(hooks) {
  setupTest(hooks);

  test('it starts empty', function(assert) {
    let cart = this.owner.lookup('service:cart');
    assert.equal(cart.itemCount, 0);
    assert.equal(cart.total, 0);
  });

  test('it adds items', function(assert) {
    let cart = this.owner.lookup('service:cart');
    cart.addItem({ id: 1, name: 'Widget', price: 10 }, 2);
    assert.equal(cart.itemCount, 2);
    assert.equal(cart.subtotal, 20);
  });

  test('it removes items', function(assert) {
    let cart = this.owner.lookup('service:cart');
    cart.addItem({ id: 1, name: 'Widget', price: 10 }, 1);
    cart.addItem({ id: 2, name: 'Gadget', price: 20 }, 1);
    cart.removeItem(1);
    assert.equal(cart.itemCount, 1);
  });

  test('it applies discounts', function(assert) {
    let cart = this.owner.lookup('service:cart');
    cart.addItem({ id: 1, name: 'Widget', price: 100 }, 1);
    cart.applyDiscount('SAVE10');
    assert.equal(cart.discount, 10);
    assert.equal(cart.total, 90);
  });
});

Common Mistakes

  1. Putting component-specific state in services. Services should hold shared state. Local UI state (form inputs, dropdown open/close) belongs in the component.
  2. Mutating tracked arrays directly. Use spread or filter to create new arrays. Direct mutation with push does not trigger updates.
  3. Not using @action on service methods. Without @action, method bindings may be lost when passed as callbacks.
  4. Over-injecting services. A component that injects 5+ services is doing too much. Delegate logic to dedicated services.
  5. Not resetting service state on logout. Services persist. When the user logs out, reset all service state to prevent data leakage between sessions.

Practice Questions

  1. What is a service in Ember?
  2. How do you inject a service into a component?
  3. How do services maintain state across route transitions?
  4. What is the difference between a service and a component?
  5. Challenge: Create three services: AuthService (login, logout, token management, user info), CartService (add, remove, clear items, apply coupons), and AnalyticsService (track page views, events, user actions). Wire them together so that logging out clears the cart. Inject all three into a Dashboard component.

FAQ

Are services singletons?

Yes. One instance per application. The same instance is injected everywhere.

Can services depend on other services?

Yes. Use @service to inject services into other services.

Do services survive route transitions?

Yes. Services persist for the entire application lifetime.

How do I reset service state?

Create a reset() method and call it when needed (e.g., on logout).

Can services have async initialization?

Yes. Use the constructor to start async initialization. Handle loading states in components.

Mini Project

Build a complete application with three services: (1) SessionService — manages authentication, provides user info, persists token to localStorage. (2) NotificationService — queue of notifications with auto-dismiss. (3) ThemeService — manages dark/light theme, persists preference. Create components that use each service. Add a settings panel to switch themes.

What's Next

Now that you understand services, learn Ember Controllers for route-specific logic. Then explore Ember Actions for event handling.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro