Architecting Scalable and Maintainable Applications

Abdullah AlHabal
4 min readSep 17, 2024

--

As developers at @TechPundits and @TeknoClass, me team and I we are not just writing code; we’re crafting the foundation of scalable, maintainable applications. I’ll share the best practices we’ve adopted in our company projects, focusing on Laravel and PHP. We’ll explore how to structure your application using interfaces, repositories, services, and DTOs, and how to leverage dependency injection for more flexible, testable code.

The Architecture Overview

Before diving into the details, let’s look at the high-level architecture we use:

  1. Interfaces: Define contracts for our repositories and services.
  2. Repositories: Handle data access logic.
  3. Services: Encapsulate business logic.
  4. DTOs (Data Transfer Objects): Represent data structures.
  5. Enums: Define sets of named constants.
  6. Providers: Bind interfaces to their implementations.

This architecture helps us adhere to SOLID principles, particularly the Dependency Inversion Principle, making our code more modular and easier to maintain.

Interfaces: The Contract

Interfaces define the methods that classes must implement. They act as a contract between different parts of your application.

File location: app/Interfaces/UserRepositoryInterface.php

<?php

namespace App\Interfaces;

interface UserRepositoryInterface
{
public function getAllUsers();
public function getUserById($id);
public function createUser(array $userDetails);
public function updateUser($id, array $newDetails);
public function deleteUser($id);
}

Repositories: Data Access Layer

Repositories implement the interfaces and handle all data access logic. This separation allows us to change the data source without affecting the rest of the application.

File location: app/Repositories/UserRepository.php

<?php

namespace App\Repositories;

use App\Interfaces\UserRepositoryInterface;
use App\Models\User;

class UserRepository implements UserRepositoryInterface
{
public function getAllUsers()
{
return User::all();
}

public function getUserById($id)
{
return User::findOrFail($id);
}

public function createUser(array $userDetails)
{
return User::create($userDetails);
}

public function updateUser($id, array $newDetails)
{
return User::whereId($id)
->update($newDetails);
}

public function deleteUser($id)
{
User::destroy($id);
}
}

Services: Business Logic Layer

Services contain the business logic of your application. They use repositories to access data but don’t know how the data is stored or retrieved.

File location: app/Services/UserService.php.

<?php

namespace App\Services;

use App\Interfaces\UserRepositoryInterface;
use App\DTOs\UserDTO;

class UserService
{
protected $userRepository;

public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}

public function createUser(UserDTO $userDTO)
{
// Perform any business logic here
$userData = $userDTO->toArray();
return $this->userRepository->createUser($userData);
}

// Other methods...
}

DTOs: Structured Data Transfer

DTOs help us pass data between processes, ensuring type safety and providing a clear contract for data structures.

File location: app/DTOs/UserDTO.php.

<?php

namespace App\DTOs;

class UserDTO
{
public function __construct(
public string $name,
public string $email,
public ?string $password = null
) {}

public function toArray(): array
{
return [
'name' => $this->name,
'email' => $this->email,
'password' => $this->password,
];
}

public static function fromArray(array $data): self
{
return new self(
$data['name'],
$data['email'],
$data['password'] ?? null
);
}

// we can add fromRequest method and associated it with custome FormRequest
}

Enums: Type-Safe Constants

Enums provide a way to define a set of named constants, which can be used to represent fixed sets of values.

File location: app/Enums/UserRole.php

<?php

namespace App\Enums;

enum UserRole: string
{
case ADMIN = 'admin';
case EDITOR = 'editor';
case USER = 'user';
}

Providers: Binding It All Together

Providers are where we bind our interfaces to their concrete implementations. This is where the magic of dependency injection happens.

File location: app/Providers/RepositoryServiceProvider.php.

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Interfaces\UserRepositoryInterface;
use App\Repositories\UserRepository;

class RepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
}
}

Don’t forget to register this provider in config/app.php:

'providers' => [
// Other providers...
App\Providers\RepositoryServiceProvider::class,
],

Putting It All Together: The Controller

Now, let’s see how all these components work together in a controller:

File location: app/Http/Controllers/UserController.php

<?php

namespace App\Http\Controllers;

use App\Services\UserService;
use App\DTOs\UserDTO;
use Illuminate\Http\Request;

class UserController extends Controller
{
protected UserService $userService;

public function __construct(UserService $userService)
{
$this->userService = $userService;
}

public function store(Request $request)
{
$validatedData = $request->validate([
'name' => 'required|string',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);

// we can move the validation into separated FormRequest.

$userDTO = UserDTO::fromArray($validatedData);

$user = $this->userService->createUser($userDTO);

return response()->json($user, 201);
}

// Other methods...
}

Benefits

  1. Separation of Concerns: Each component has a clear, single responsibility.
  2. Testability: Easy to mock dependencies for unit testing.
  3. Flexibility: Easy to swap implementations without changing the consuming code.
  4. Scalability: As the project grows, it’s easier to maintain and extend.
  5. Type Safety: DTOs and Enums provide clear contracts and reduce the chances of errors

How to Structure Your Files

Here’s a recommended file structure for this architecture:

app/
├── DTOs/
├── Enums/
├── Http/
│ └── Controllers/
├── Interfaces/
├── Models/
├── Providers/
├── Repositories/
└── Services/

Useful Artisan Commands

Here are some Artisan commands to help you create these components:

# Create a new interface
php artisan make:interface UserRepositoryInterface

# Create a new repository
php artisan make:repository UserRepository

# Create a new service
php artisan make:service UserService

# Create a new DTO
php artisan make:dto UserDTO

# Create a new enum
php artisan make:enum UserRole

# Create a new provider
php artisan make:provider RepositoryServiceProvider

Note: Some of these commands

(like make:interface, make:repository, make:service, and make:dto)

are not available in Laravel by default. You might need to create custom Artisan commands or use packages that provide these commands.

Conclusion

By adopting these practices, we’ve created a robust, scalable architecture for our Laravel applications. This approach allows us to write clean, maintainable code that’s easy to test and extend. Remember, good code is self-documenting. With this architecture, our APIs and application structure speak for themselves, making it easier for developers to understand and work with the codebase.

Let’s Keep Coding! 😉👨🏻‍💻

--

--

Abdullah AlHabal

Junior Backend Software Engineer | Laravel, PHP, NestJS | API Design | MySQL, PostgreSQL | Docker | Web Technologies Enthusiast