This is an introduction to algorithmic aspects of distributed computing. The topics considered are those that arise in systems comprised of loosely coupled, heterogeneous and failure-prone processing units. The range of applications starts at wide-area networks, goes through clusters of workstations connected by local-area networks, to multi-processor shared-memory machines. The relevant properties of solutions reflect the communication mechanisms (message passing, shared memory), the algorithmic constraints (deterministic, randomized), the timing models (synchronous, asynchronous), and the types of failures (crashes, Byzantine). The algorithmic goals to achieve include: sharing resources in a fair manner, providing fault-tolerance, and maintaining global consistency of computations. The specific problems include: symmetry breaking, consensus, resource allocation, renaming, and synchronization.