Najlepsze Pytania o Wzorce i Architekturę - Java, Spring, Node.js, Express

Sławomir Plamowski 19 min czytania
architektura backend express java nodejs spring wzorce-projektowe

W tym artykule zebrałem najlepsze pytania o wzorce projektowe i architekturę z czterech technologii backend: Java, Spring Framework, Node.js i Express.js. To pytania, które sam zadaję kandydatom i które słyszałem na rozmowach w firmach od startupów po korporacje. Każde pytanie ma dwie wersje odpowiedzi - krótką na 30 sekund (gdy rekruter chce szybko sprawdzić podstawy) i rozszerzoną na 2 minuty (gdy chce zagłębić się w temat).

Fundamenty: Inversion of Control i Dependency Injection

Czym jest IoC i DI? Jaka jest między nimi różnica?

Odpowiedź w 30 sekund:

Inversion of Control to zasada projektowa, gdzie kontrola nad tworzeniem i zarządzaniem obiektami jest przenoszona z kodu aplikacji do zewnętrznego kontenera. Dependency Injection to konkretna implementacja IoC - kontener wstrzykuje zależności do obiektu przez konstruktor, setter lub pole. IoC to idea, DI to technika jej realizacji.

Odpowiedź w 2 minuty:

Wyobraź sobie tradycyjny kod, gdzie klasa OrderService sama tworzy instancję PaymentGateway:

// Bez IoC - klasa sama zarządza zależnościami
public class OrderService {
    // Silne powiązanie - trudne do testowania
    private PaymentGateway paymentGateway = new StripePaymentGateway();

    public void processOrder(Order order) {
        paymentGateway.charge(order.getTotal());
    }
}

Problem? Nie możesz łatwo podmienić StripePaymentGateway na PayPalPaymentGateway w testach. Nie możesz też zmienić implementacji bez modyfikacji OrderService.

Inversion of Control odwraca tę kontrolę - to nie klasa decyduje jakich zależności użyje, tylko zewnętrzny kontener dostarcza jej gotowe obiekty:

// Z DI - zależność wstrzykiwana przez konstruktor
public class OrderService {
    private final PaymentGateway paymentGateway;

    // Konstruktor przyjmuje interfejs, nie konkretną implementację
    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    public void processOrder(Order order) {
        paymentGateway.charge(order.getTotal());
    }
}

Teraz OrderService nie wie i nie dba, czy dostaje Stripe czy PayPal - zależy tylko od interfejsu. W testach możesz wstrzyknąć mock:

// Łatwe testowanie z mockiem
@Test
void shouldChargeCorrectAmount() {
    PaymentGateway mockGateway = mock(PaymentGateway.class);
    OrderService service = new OrderService(mockGateway);

    service.processOrder(new Order(100.0));

    verify(mockGateway).charge(100.0);
}

W Spring kontener IoC (ApplicationContext) automatycznie wykrywa zależności i je wstrzykuje. W Node.js możesz użyć bibliotek jak InversifyJS lub stosować DI ręcznie przez konstruktory.

flowchart TB subgraph "Bez IoC (Tight Coupling)" A1[OrderService] -->|tworzy| B1[StripePaymentGateway] A1 -->|tworzy| C1[EmailService] end subgraph "Z IoC (Loose Coupling)" Container[IoC Container] -->|wstrzykuje| A2[OrderService] Container -->|zarządza| B2[PaymentGateway] Container -->|zarządza| C2[EmailService] A2 -.->|używa interfejsu| B2 A2 -.->|używa interfejsu| C2 end style Container fill:#fff3e0 style A1 fill:#ffcdd2 style A2 fill:#c8e6c9

Spring Framework: Architektura i Wzorce

Jak działa kontener IoC w Spring i jakie są różnice między BeanFactory a ApplicationContext?

Odpowiedź w 30 sekund:

BeanFactory to podstawowy kontener IoC - tworzy i zarządza beanami, wspiera lazy loading. ApplicationContext rozszerza BeanFactory o: eager loading domyślnie, obsługę zdarzeń, internacjonalizację (i18n), integrację z AOP. W praktyce zawsze używamy ApplicationContext - BeanFactory jest zbyt podstawowy dla realnych aplikacji.

Odpowiedź w 2 minuty:

BeanFactory to interfejs definiujący podstawowe operacje kontenera: getBean(), sprawdzanie czy bean istnieje, pobieranie typu. To minimum potrzebne do Dependency Injection:

// BeanFactory - podstawowe API
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
MyService service = factory.getBean("myService", MyService.class);

ApplicationContext to "bogaty kuzyn" BeanFactory. Dziedziczy wszystkie jego możliwości i dodaje funkcje enterprise:

// ApplicationContext - pełny kontener Spring
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

// Wszystko co BeanFactory plus:
// - Automatyczne wykrywanie @Component, @Service, @Repository
// - Obsługa zdarzeń aplikacji
// - Dostęp do zasobów (pliki, URL)
// - Internacjonalizacja (MessageSource)
// - Integracja z AOP

Kluczowa różnica w zachowaniu - domyślnie BeanFactory tworzy beany lazy (przy pierwszym użyciu), ApplicationContext tworzy singleton beany przy starcie. To pozwala wykryć błędy konfiguracji wcześniej:

// Przy starcie ApplicationContext - błąd zostanie wykryty od razu
// Przy BeanFactory - błąd dopiero gdy ktoś poprosi o tego beana
@Service
public class OrderService {
    @Autowired
    private PaymentGateway gateway; // Brak implementacji?
    // ApplicationContext: błąd przy starcie
    // BeanFactory: błąd przy pierwszym getBean()
}

W Spring Boot nigdy nie tworzysz kontenera ręcznie - @SpringBootApplication robi to za Ciebie, tworząc AnnotationConfigServletWebServerApplicationContext dla aplikacji webowych.

Porównaj adnotacje @Component, @Service, @Repository i @Controller

Odpowiedź w 30 sekund:

Wszystkie cztery są wykrywane przez component scanning i tworzą beany Spring. @Component to generyczna adnotacja. @Service oznacza warstwę logiki biznesowej. @Repository oznacza warstwę DAO i dodatkowo tłumaczy wyjątki bazodanowe. @Controller oznacza kontroler MVC obsługujący żądania HTTP. Różnią się semantyką i dodatkowymi funkcjami.

Odpowiedź w 2 minuty:

Wszystkie te adnotacje dziedziczą po @Component - Spring traktuje je jako "stereotypy" oznaczające różne warstwy aplikacji:

// Ogólny komponent - gdy nie pasuje do żadnej warstwy
@Component
public class EmailValidator {
    public boolean isValid(String email) {
        return email.contains("@");
    }
}

// Warstwa logiki biznesowej
@Service
public class OrderService {
    private final OrderRepository repository;
    private final PaymentGateway gateway;

    public Order createOrder(OrderRequest request) {
        // Logika biznesowa
    }
}

// Warstwa dostępu do danych
@Repository
public class JpaOrderRepository implements OrderRepository {
    @PersistenceContext
    private EntityManager em;

    public Order findById(Long id) {
        return em.find(Order.class, id);
    }
}

// Warstwa prezentacji (kontroler HTTP)
@Controller
public class OrderController {
    @GetMapping("/orders/{id}")
    public String showOrder(@PathVariable Long id, Model model) {
        model.addAttribute("order", orderService.findById(id));
        return "order-details"; // nazwa widoku
    }
}

@Repository ma specjalną funkcję - automatycznie tłumaczy wyjątki specyficzne dla bazy danych (SQLException, HibernateException) na ujednoliconą hierarchię DataAccessException Spring:

@Repository
public class JdbcUserRepository {
    public User findById(Long id) {
        // SQLException zostanie automatycznie przetłumaczony na
        // DataAccessException bez dodatkowego kodu
    }
}

@RestController to @Controller + @ResponseBody - metody zwracają dane bezpośrednio (JSON/XML) zamiast nazwy widoku:

@RestController
@RequestMapping("/api")
public class OrderApiController {
    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable Long id) {
        return orderService.findById(id); // Automatyczna serializacja do JSON
    }
}
flowchart TB subgraph "Hierarchia stereotypów Spring" Component["@Component
(bazowy)"] Service["@Service
(logika biznesowa)"] Repository["@Repository
(dostęp do danych)
+ tłumaczenie wyjątków"] Controller["@Controller
(warstwa web)
+ obsługa żądań HTTP"] RestController["@RestController
= @Controller + @ResponseBody"] Component --> Service Component --> Repository Component --> Controller Controller --> RestController end style Component fill:#e3f2fd style Service fill:#fff3e0 style Repository fill:#e8f5e9 style Controller fill:#fce4ec style RestController fill:#f3e5f5

Jak działa @Transactional i jakie są pułapki?

Odpowiedź w 30 sekund:

@Transactional oznacza metodę lub klasę jako transakcyjną - Spring automatycznie rozpoczyna transakcję przed wywołaniem i commituje po zakończeniu (lub rollback przy wyjątku). Działa przez proxy AOP. Główne pułapki: nie działa dla wywołań wewnętrznych (self-invocation), domyślnie rollback tylko dla RuntimeException, proxy nie przechwytuje prywatnych metod.

Odpowiedź w 2 minuty:

Spring tworzy proxy wokół klasy z @Transactional. Gdy wywołujesz metodę transakcyjną, proxy:

  1. Otwiera transakcję
  2. Wywołuje oryginalną metodę
  3. Commituje (sukces) lub rollback (wyjątek)
@Service
public class OrderService {

    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order(request);
        orderRepository.save(order);

        // Jeśli tu poleci RuntimeException - cała transakcja zostanie wycofana
        paymentService.charge(order);

        return order;
    }
}

Tu robi się ciekawie - pułapka numer jeden, self-invocation:

@Service
public class OrderService {

    public void processOrders(List<OrderRequest> requests) {
        for (OrderRequest req : requests) {
            createOrder(req); // NIE DZIAŁA! Wywołanie wewnętrzne omija proxy
        }
    }

    @Transactional
    public Order createOrder(OrderRequest request) {
        // Ta transakcja NIE zostanie utworzona przy wywołaniu z processOrders
    }
}

Rozwiązanie? Wstrzyknij serwis sam do siebie lub wydziel do osobnej klasy:

@Service
public class OrderService {
    @Autowired
    private OrderService self; // Wstrzyknięcie proxy samego siebie

    public void processOrders(List<OrderRequest> requests) {
        for (OrderRequest req : requests) {
            self.createOrder(req); // Teraz przechodzi przez proxy
        }
    }
}

Pułapka numer dwa - checked exceptions nie powodują rollback domyślnie:

@Transactional
public void transfer(Account from, Account to, BigDecimal amount)
        throws InsufficientFundsException { // Checked exception

    from.debit(amount);
    to.credit(amount);

    if (from.getBalance().compareTo(BigDecimal.ZERO) < 0) {
        throw new InsufficientFundsException(); // Transakcja NIE zostanie wycofana!
    }
}

// Rozwiązanie - jawne określenie rollback
@Transactional(rollbackFor = InsufficientFundsException.class)
public void transfer(...) throws InsufficientFundsException {
    // Teraz rollback nastąpi również dla tego wyjątku
}

Node.js: Event Loop i Architektura Asynchroniczna

Wyjaśnij jak działa Event Loop i dlaczego Node.js jest jednowątkowy

Odpowiedź w 30 sekund:

Event Loop to mechanizm zarządzający asynchronicznością w Node.js. Główny wątek wykonuje kod JavaScript, ale operacje I/O (baza, pliki, sieć) są delegowane do puli wątków libuv. Gdy operacja się kończy, callback trafia do kolejki. Event Loop pobiera callbacki gdy stos wywołań jest pusty. Jednowątkowość eliminuje problemy z synchronizacją, ale wymaga unikania blokujących operacji.

Odpowiedź w 2 minuty:

Node.js używa modelu "single-threaded event loop with non-blocking I/O". Brzmi skomplikowanie, ale idea jest prosta:

// Wyobraź sobie restaurację z jednym kelnerem (główny wątek)
// Kelner przyjmuje zamówienia (żądania HTTP)
// Kuchnia to pula wątków libuv (operacje I/O)

console.log('1. Kelner przyjmuje zamówienie'); // Synchroniczne - natychmiast

// Zamówienie idzie do kuchni (asynchroniczne I/O)
fs.readFile('menu.txt', (err, data) => {
    console.log('3. Danie gotowe, kelner przynosi'); // Callback - później
});

console.log('2. Kelner idzie do następnego stolika'); // Synchroniczne - zaraz po 1

Output: 1, 2, 3 - nie 1, 3, 2. Kelner nie czeka przy kuchni.

Event Loop ma fazy:

flowchart TB subgraph "Event Loop - Fazy" Timers["1. Timers
setTimeout, setInterval"] Pending["2. Pending Callbacks
I/O callbacks"] Poll["3. Poll
Nowe I/O events"] Check["4. Check
setImmediate"] Close["5. Close Callbacks
socket.on('close')"] Timers --> Pending Pending --> Poll Poll --> Check Check --> Close Close --> Timers end MicroTasks["Microtasks Queue
process.nextTick, Promise.then"] MicroTasks -.->|"wykonywane między fazami"| Timers MicroTasks -.-> Pending MicroTasks -.-> Poll style Poll fill:#fff3e0 style MicroTasks fill:#e8f5e9

Pułapka - blokowanie Event Loop:

// ZŁY KOD - blokuje Event Loop
app.get('/hash', (req, res) => {
    // Synchroniczne hashowanie - blokuje WSZYSTKIE żądania
    const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512');
    res.send(hash);
});

// DOBRY KOD - nie blokuje
app.get('/hash', async (req, res) => {
    // Asynchroniczne - idzie do puli wątków
    const hash = await crypto.pbkdf2(password, salt, 100000, 64, 'sha512');
    res.send(hash);
});

Zasada: operacje CPU-intensive (hashowanie, kompresja, obliczenia) przenieś do Worker Threads lub osobnego procesu.

Czym różni się CommonJS od ES Modules i kiedy używać którego?

Odpowiedź w 30 sekund:

CommonJS (require/module.exports) to tradycyjny system modułów Node.js - synchroniczny, dynamiczny (można require w runtime). ES Modules (import/export) to standard ECMAScript - asynchroniczny, statyczny (analizowany przed wykonaniem). CommonJS dla legacy kodu i prostych skryptów. ESM dla nowych projektów - lepsze tree-shaking, top-level await, kompatybilność z przeglądarkami.

Odpowiedź w 2 minuty:

// CommonJS - tradycyjny Node.js
const express = require('express');
const { readFile } = require('fs');

module.exports = {
    startServer: function() { /* ... */ }
};

// Można dynamicznie
if (process.env.USE_CACHE) {
    const cache = require('./cache'); // Warunkowy import
}
// ES Modules - nowoczesny standard
import express from 'express';
import { readFile } from 'fs/promises';

export function startServer() { /* ... */ }

// Dynamiczny import (zwraca Promise)
if (process.env.USE_CACHE) {
    const { cache } = await import('./cache.js');
}

Kluczowe różnice:

Aspekt CommonJS ES Modules
Ładowanie Synchroniczne Asynchroniczne
Analiza Runtime Przed wykonaniem (static)
Top-level await Nie Tak
this w module exports undefined
Rozszerzenie .js (domyślne) .mjs lub "type": "module"
Tree-shaking Słabe Dobre

Kiedy ESM? Nowe projekty, biblioteki publikowane na npm, kod współdzielony z frontendem.

Kiedy CommonJS? Legacy projekty, skrypty CLI, gdy używasz bibliotek tylko-CommonJS.

// package.json dla ESM
{
    "type": "module",
    "exports": {
        ".": "./src/index.js"
    }
}

Express.js: Middleware i Architektura Aplikacji

Czym jest middleware i jak zorganizować go w dużej aplikacji?

Odpowiedź w 30 sekund:

Middleware to funkcja z dostępem do req, res i next. Przetwarza żądanie, może je zmodyfikować, zakończyć odpowiedzią lub przekazać dalej. W dużej aplikacji: podziel na moduły (auth, validation, errorHandler), używaj Router dla grupowania tras, globalny error handler na końcu, zachowaj właściwą kolejność (parsery -> auth -> routes -> errors).

Odpowiedź w 2 minuty:

Middleware to serce Express. Każde żądanie przechodzi przez łańcuch funkcji:

// Anatomia middleware
function myMiddleware(req, res, next) {
    // 1. Zrób coś z req/res
    console.log(`${req.method} ${req.url}`);

    // 2. Albo zakończ odpowiedzią
    // res.status(401).send('Unauthorized');

    // 3. Albo przekaż dalej
    next();

    // 4. Lub przekaż błąd
    // next(new Error('Something went wrong'));
}

Dla dużej aplikacji - struktura katalogów:

src/
├── middleware/
│   ├── auth.js          # Weryfikacja JWT/sesji
│   ├── validation.js    # Walidacja body
│   ├── rateLimit.js     # Ograniczanie requestów
│   └── errorHandler.js  # Centralny handler błędów
├── routes/
│   ├── index.js         # Agreguje wszystkie routery
│   ├── users.js         # /api/users/*
│   └── orders.js        # /api/orders/*
├── controllers/
│   ├── userController.js
│   └── orderController.js
└── app.js               # Konfiguracja Express
// app.js - właściwa kolejność middleware
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');

const authMiddleware = require('./middleware/auth');
const errorHandler = require('./middleware/errorHandler');
const routes = require('./routes');

const app = express();

// 1. Security middleware (najpierw!)
app.use(helmet());
app.use(cors());

// 2. Parsery body
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// 3. Logging
app.use((req, res, next) => {
    console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
    next();
});

// 4. Routes (z auth gdzie potrzeba)
app.use('/api', routes);

// 5. 404 handler
app.use((req, res) => {
    res.status(404).json({ error: 'Not Found' });
});

// 6. Error handler (ZAWSZE ostatni!)
app.use(errorHandler);
// middleware/errorHandler.js - centralny handler błędów
function errorHandler(err, req, res, next) {
    console.error(err.stack);

    // Rozróżnianie typów błędów
    if (err.name === 'ValidationError') {
        return res.status(400).json({
            error: 'Validation Error',
            details: err.details
        });
    }

    if (err.name === 'UnauthorizedError') {
        return res.status(401).json({ error: 'Invalid token' });
    }

    // Domyślny błąd serwera
    res.status(500).json({
        error: process.env.NODE_ENV === 'production'
            ? 'Internal Server Error'
            : err.message
    });
}

module.exports = errorHandler;
flowchart LR Request[Żądanie HTTP] --> Security[helmet/cors] Security --> Parsers[JSON/URL parser] Parsers --> Logger[Logging] Logger --> Auth{Auth?} Auth -->|Protected| AuthMW[Auth Middleware] Auth -->|Public| Routes AuthMW --> Routes[Route Handlers] Routes --> Response[Odpowiedź] Routes -->|Error| ErrorHandler[Error Handler] ErrorHandler --> Response style Security fill:#ffcdd2 style Auth fill:#fff3e0 style ErrorHandler fill:#e8f5e9

Jak zaimplementować Dependency Injection w Express bez frameworka?

Odpowiedź w 30 sekund:

Bez frameworka IoC używaj constructor injection: twórz instancje serwisów w jednym miejscu (composition root), wstrzykuj przez konstruktory kontrolerów/route handlerów. Alternatywnie: factory functions zwracające skonfigurowane handlery. Unikaj globalnych singletonów - utrudniają testowanie.

Odpowiedź w 2 minuty:

Express nie wymusza DI, ale możesz go zaimplementować ręcznie:

// services/userService.js
class UserService {
    constructor(userRepository, emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }

    async createUser(data) {
        const user = await this.userRepository.create(data);
        await this.emailService.sendWelcome(user.email);
        return user;
    }
}

module.exports = UserService;
// controllers/userController.js - factory function
function createUserController(userService) {
    return {
        async create(req, res, next) {
            try {
                const user = await userService.createUser(req.body);
                res.status(201).json(user);
            } catch (err) {
                next(err);
            }
        },

        async getById(req, res, next) {
            try {
                const user = await userService.findById(req.params.id);
                if (!user) return res.status(404).json({ error: 'Not found' });
                res.json(user);
            } catch (err) {
                next(err);
            }
        }
    };
}

module.exports = createUserController;
// composition-root.js - tutaj składamy wszystko
const UserRepository = require('./repositories/userRepository');
const EmailService = require('./services/emailService');
const UserService = require('./services/userService');
const createUserController = require('./controllers/userController');
const createUserRouter = require('./routes/users');

function createContainer(config) {
    // Tworzymy instancje w odpowiedniej kolejności
    const userRepository = new UserRepository(config.db);
    const emailService = new EmailService(config.smtp);
    const userService = new UserService(userRepository, emailService);
    const userController = createUserController(userService);
    const userRouter = createUserRouter(userController);

    return {
        userRouter,
        // Expose dla testów
        userService,
        userRepository
    };
}

module.exports = createContainer;
// app.js
const express = require('express');
const createContainer = require('./composition-root');
const config = require('./config');

const app = express();
const container = createContainer(config);

app.use(express.json());
app.use('/api/users', container.userRouter);

module.exports = app;

Teraz testy są proste:

// tests/userService.test.js
const UserService = require('../services/userService');

describe('UserService', () => {
    it('should send welcome email after creating user', async () => {
        // Mock dependencies
        const mockRepo = { create: jest.fn().mockResolvedValue({ id: 1, email: 'test@test.com' }) };
        const mockEmail = { sendWelcome: jest.fn().mockResolvedValue() };

        const service = new UserService(mockRepo, mockEmail);
        await service.createUser({ email: 'test@test.com' });

        expect(mockEmail.sendWelcome).toHaveBeenCalledWith('test@test.com');
    });
});

Wzorce Projektowe w Praktyce

Kompozycja vs Dziedziczenie - kiedy używać którego?

Odpowiedź w 30 sekund:

Dziedziczenie dla relacji "jest" (is-a): Dog jest Animal. Kompozycja dla relacji "ma" (has-a): Car ma Engine. Preferuj kompozycję - daje elastyczność runtime, unika problemów z głęboką hierarchią, ułatwia testowanie. Zasada Gang of Four: "Favor composition over inheritance".

Odpowiedź w 2 minuty:

// Dziedziczenie - gdy naprawdę "jest" (is-a)
public abstract class Animal {
    protected String name;

    public abstract void makeSound();

    public void sleep() {
        System.out.println(name + " śpi");
    }
}

public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Hau hau!");
    }

    public void fetch() {
        System.out.println(name + " przynosi piłkę");
    }
}

// Dog JEST Animal - to ma sens
Animal pet = new Dog();
pet.makeSound(); // Polimorfizm działa

Problem z dziedziczeniem - co jeśli potrzebujesz "pływającego psa" i "latającego psa"?

// Problemy z dziedziczeniem
public class FlyingDog extends Dog { /* jak odziedziczyć latanie? */ }
public class SwimmingDog extends Dog { /* a pływanie? */ }
public class FlyingSwimmingDog extends ??? { /* Java nie ma wielodziedziczenia! */ }

Rozwiązanie - kompozycja z interfejsami:

// Kompozycja - elastyczność przez składanie
public interface Flyable {
    void fly();
}

public interface Swimmable {
    void swim();
}

public class Dog {
    private String name;
    private Flyable flyBehavior;      // Może być null
    private Swimmable swimBehavior;   // Może być null

    public Dog(String name) {
        this.name = name;
    }

    public void setFlyBehavior(Flyable flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    public void performFly() {
        if (flyBehavior != null) {
            flyBehavior.fly();
        } else {
            System.out.println(name + " nie umie latać");
        }
    }
}

// Implementacje zachowań
public class JetpackFlying implements Flyable {
    public void fly() {
        System.out.println("Leci z jetpackiem!");
    }
}

// Użycie
Dog superDog = new Dog("Rex");
superDog.setFlyBehavior(new JetpackFlying()); // Zmiana w runtime!
superDog.performFly();

W Node.js/JavaScript - preferuj kompozycję obiektów:

// Kompozycja przez mixiny/Object.assign
const canFly = {
    fly() {
        console.log(`${this.name} leci!`);
    }
};

const canSwim = {
    swim() {
        console.log(`${this.name} pływa!`);
    }
};

function createDog(name) {
    return {
        name,
        bark() {
            console.log('Hau hau!');
        }
    };
}

// Składamy super-psa
const superDog = Object.assign(
    createDog('Rex'),
    canFly,
    canSwim
);

superDog.bark();  // Hau hau!
superDog.fly();   // Rex leci!
superDog.swim();  // Rex pływa!

Jak zaimplementować wzorzec Repository w Spring i Express?

Odpowiedź w 30 sekund:

Repository to warstwa abstrakcji nad dostępem do danych. W Spring: interfejs rozszerzający JpaRepository/CrudRepository - Spring generuje implementację. W Express: klasa/moduł enkapsulujący operacje na bazie, wstrzykiwany do serwisów. Cel: oddzielenie logiki biznesowej od szczegółów persystencji, łatwiejsze testowanie i podmiana źródła danych.

Odpowiedź w 2 minuty:

Spring Data JPA - minimalna implementacja:

// Interfejs - Spring generuje implementację
public interface OrderRepository extends JpaRepository<Order, Long> {

    // Query Methods - Spring generuje zapytanie z nazwy
    List<Order> findByStatus(OrderStatus status);

    List<Order> findByCustomerIdOrderByCreatedAtDesc(Long customerId);

    // Custom query gdy nazwa byłaby za długa
    @Query("SELECT o FROM Order o WHERE o.total > :minTotal AND o.status = :status")
    List<Order> findExpensiveOrdersByStatus(
        @Param("minTotal") BigDecimal minTotal,
        @Param("status") OrderStatus status
    );
}

// Użycie w serwisie
@Service
public class OrderService {
    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public List<Order> getCustomerOrders(Long customerId) {
        return orderRepository.findByCustomerIdOrderByCreatedAtDesc(customerId);
    }
}

Express z czystym JavaScript:

// repositories/orderRepository.js
class OrderRepository {
    constructor(db) {
        this.db = db; // Instancja połączenia z bazą (Knex, Prisma, Mongoose...)
    }

    async findById(id) {
        return this.db('orders').where({ id }).first();
    }

    async findByCustomerId(customerId) {
        return this.db('orders')
            .where({ customer_id: customerId })
            .orderBy('created_at', 'desc');
    }

    async create(orderData) {
        const [id] = await this.db('orders').insert(orderData);
        return this.findById(id);
    }

    async updateStatus(id, status) {
        await this.db('orders').where({ id }).update({ status });
        return this.findById(id);
    }
}

module.exports = OrderRepository;
// services/orderService.js
class OrderService {
    constructor(orderRepository, paymentService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
    }

    async createOrder(customerId, items) {
        const total = this.calculateTotal(items);

        const order = await this.orderRepository.create({
            customer_id: customerId,
            items: JSON.stringify(items),
            total,
            status: 'pending'
        });

        return order;
    }

    async getCustomerOrders(customerId) {
        return this.orderRepository.findByCustomerId(customerId);
    }
}

module.exports = OrderService;

Korzyść - łatwe mockowanie w testach:

// tests/orderService.test.js
describe('OrderService', () => {
    it('should create order with correct total', async () => {
        const mockRepo = {
            create: jest.fn().mockResolvedValue({ id: 1, total: 150 }),
            findById: jest.fn()
        };

        const service = new OrderService(mockRepo, {});

        await service.createOrder(1, [
            { productId: 1, price: 100 },
            { productId: 2, price: 50 }
        ]);

        expect(mockRepo.create).toHaveBeenCalledWith(
            expect.objectContaining({ total: 150 })
        );
    });
});

Na Co Rekruterzy Naprawdę Zwracają Uwagę

Po przeprowadzeniu setek rozmów rekrutacyjnych na stanowiska backend, zauważyłem wzorce w odpowiedziach, które wyróżniają kandydatów:

1. Rozumienie "dlaczego", nie tylko "jak"

Słaba odpowiedź: "Używamy @Transactional żeby mieć transakcje." Dobra odpowiedź: "Używamy @Transactional, bo dzięki proxy AOP możemy deklaratywnie zarządzać transakcjami bez boilerplate'u. Ważne jest zrozumienie, że nie działa to dla wywołań wewnętrznych, bo omijają proxy."

2. Świadomość trade-offów

Słaba odpowiedź: "Kompozycja jest lepsza od dziedziczenia." Dobra odpowiedź: "Kompozycja daje elastyczność i ułatwia testowanie, ale dziedziczenie jest naturalne dla hierarchii 'jest' - Dog jest Animal. Problem zaczyna się przy głębokich hierarchiach i potrzebie wielokrotnego dziedziczenia. Preferuję kompozycję, ale nie dogmatycznie."

3. Praktyczne doświadczenie z problemami

Kandydat, który powie "Kiedyś spędziłem dzień debugując dlaczego @Transactional nie działało - okazało się, że metoda była wywoływana wewnętrznie i omijała proxy" - pokazuje realne doświadczenie, nie tylko teorię.

4. Umiejętność wyjaśniania prostym językiem

Najlepsi seniorzy potrafią wyjaśnić Event Loop w Node.js używając analogii do restauracji z jednym kelnerem. Skomplikowane słownictwo często maskuje brak zrozumienia.

Zadania Praktyczne

Przed rozmową przećwicz te scenariusze:

1. Zaprojektuj warstwę serwisów dla systemu e-commerce

Masz: UserService, ProductService, OrderService, PaymentService, NotificationService. Jakie zależności między nimi? Jak je wstrzyknąć? Co jeśli PaymentService może używać różnych bramek płatności?

2. Zrefaktoruj ten kod używając DI:

public class ReportGenerator {
    public void generateReport() {
        DatabaseConnection db = new MySqlConnection("localhost", "reports");
        List<Data> data = db.query("SELECT * FROM sales");

        PdfWriter writer = new PdfWriter();
        writer.write(data, "report.pdf");

        EmailSender sender = new SmtpEmailSender("smtp.company.com");
        sender.send("manager@company.com", "report.pdf");
    }
}

3. Zaimplementuj middleware pipeline w Express:

Stwórz: rate limiter (max 100 req/min per IP), auth middleware (JWT), request logger, error handler. Jaka kolejność? Jak obsłużyć błędy z async handlerów?

4. Wyjaśnij co się stanie i dlaczego:

app.get('/slow', async (req, res) => {
    const result = crypto.pbkdf2Sync(req.query.password, 'salt', 1000000, 64, 'sha512');
    res.send(result);
});

// Czy inne requesty będą obsługiwane podczas wykonywania /slow?

Zobacz też


Rozwiń Swoje Umiejętności

Ten artykuł to zaledwie wierzchołek góry lodowej. Pełne przygotowanie do rozmowy o architekturę backend wymaga głębszego zrozumienia:

  • Java: 150 pytań od podstaw OOP po zaawansowane multithreading i Stream API
  • Spring: 56 pytań o IoC, AOP, Security, Data i mikroserwisy
  • Node.js: 45 pytań o Event Loop, Streams, Cluster i wydajność
  • Express: 49 pytań o middleware, routing, bezpieczeństwo i skalowanie

Uzyskaj dostęp do wszystkich pytań backend →

Lub wypróbuj bezpłatnie:


Artykuł powstał na podstawie doświadczeń z rozmów rekrutacyjnych w firmach technologicznych i ponad 10 lat praktyki w budowaniu systemów backend.

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.